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:
Matt Gibson 2021-01-25 14:27:38 -06:00 committed by GitHub
parent 9b6bf136f1
commit 8d5614cd7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 2046 additions and 38 deletions

View File

@ -63,6 +63,9 @@ jobs:
- name: Restore packages
run: nuget restore
- name: Run Core Tests
run: dotnet test test/Core.Test/Core.Test.csproj
- name: Build Play Store publisher
run: dotnet build ./store/google/Publisher/Publisher.csproj -p:Configuration=Release

View File

@ -41,6 +41,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iOS.Extension", "src\iOS.Ex
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iOS.Autofill", "src\iOS.Autofill\iOS.Autofill.csproj", "{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "test\Common\Common.csproj", "{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.Test", "test\Core.Test\Core.Test.csproj", "{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Ad-Hoc|Any CPU = Ad-Hoc|Any CPU
@ -351,6 +355,66 @@ Global
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Release|iPhone.Build.0 = Release|iPhone
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.AppStore|Any CPU.Build.0 = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.AppStore|iPhone.ActiveCfg = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.AppStore|iPhone.Build.0 = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.Debug|iPhone.Build.0 = Debug|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.FDroid|Any CPU.ActiveCfg = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.FDroid|Any CPU.Build.0 = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.FDroid|iPhone.ActiveCfg = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.FDroid|iPhone.Build.0 = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.FDroid|iPhoneSimulator.ActiveCfg = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.FDroid|iPhoneSimulator.Build.0 = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.Release|Any CPU.Build.0 = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.Release|iPhone.ActiveCfg = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.Release|iPhone.Build.0 = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.AppStore|Any CPU.ActiveCfg = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.AppStore|Any CPU.Build.0 = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.AppStore|iPhone.ActiveCfg = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.AppStore|iPhone.Build.0 = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.Debug|iPhone.Build.0 = Debug|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.FDroid|Any CPU.ActiveCfg = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.FDroid|Any CPU.Build.0 = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.FDroid|iPhone.ActiveCfg = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.FDroid|iPhone.Build.0 = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.FDroid|iPhoneSimulator.ActiveCfg = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.FDroid|iPhoneSimulator.Build.0 = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.Release|Any CPU.Build.0 = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.Release|iPhone.ActiveCfg = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.Release|iPhone.Build.0 = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -366,6 +430,8 @@ Global
{599E0201-420A-4C3E-A7BA-5349F72E0B15} = {D10CA4A9-F866-40E1-B658-F69051236C71}
{324BE76C-38FA-4F11-8BB1-95C7B3B1B545} = {D10CA4A9-F866-40E1-B658-F69051236C71}
{8A3ECD75-3EC8-4CB3-B3A2-A73A724C279A} = {D10CA4A9-F866-40E1-B658-F69051236C71}
{4085B0A5-12A9-4993-B8B8-4ACE72E62E39} = {8904C536-C67D-420F-9971-51B26574C3AA}
{8AE548D9-A567-4E97-995E-93EC7DB0FDE0} = {8904C536-C67D-420F-9971-51B26574C3AA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7D436EA3-8B7E-45D2-8D14-0730BD2E0410}

View File

@ -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">

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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);

View 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);
}
}

View File

@ -0,0 +1,8 @@
namespace Bit.Core.Enums
{
public enum HkdfAlgorithm : byte
{
Sha256 = 1,
Sha512 = 2,
}
}

View File

@ -0,0 +1,8 @@
namespace Bit.Core.Enums
{
public enum SendType
{
Text = 0,
File = 1,
}
}

View 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; }
}
}

View File

@ -0,0 +1,8 @@
namespace Bit.Core.Models.Api
{
public class SendTextApi
{
public string Text { get; set; }
public bool Hidden { get; set; }
}
}

View 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; }
}
}

View 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; }
}
}

View 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; }
}
}

View File

@ -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
{

View File

@ -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);

View 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;
}
}
}

View 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);
}
}

View 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);
}
}

View 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;
}
}
}
}

View 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; }
}
}

View File

@ -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>();
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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}");
}
}
}
}

View 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);
}
}
}
}

View File

@ -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)));
}
}

View File

@ -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));
}
}
}

View File

@ -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);

View File

@ -0,0 +1,10 @@
using AutoFixture.AutoNSubstitute;
namespace Bit.Test.Common.AutoFixture.Attributes
{
public class AutoSubstitutionData : CustomAutoDataAttribute
{
public AutoSubstitutionData() : base(typeof(AutoNSubstituteCustomization))
{ }
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Linq;
using AutoFixture;
using AutoFixture.Xunit2;
namespace Bit.Test.Common.AutoFixture.Attributes
{
public class CustomAutoDataAttribute : AutoDataAttribute
{
public CustomAutoDataAttribute(params Type[] iCustomizationTypes) : this(iCustomizationTypes
.Select(t => (ICustomization)Activator.CreateInstance(t)).ToArray())
{ }
public CustomAutoDataAttribute(params ICustomization[] customizations) : base(() =>
{
var fixture = new Fixture();
foreach (var customization in customizations)
{
fixture.Customize(customization);
}
return fixture;
})
{ }
}
}

View File

@ -0,0 +1,10 @@
using AutoFixture.AutoNSubstitute;
namespace Bit.Test.Common.AutoFixture.Attributes
{
public class InlineAutoSubstitutionData : InlineCustomAutoDataAttribute
{
public InlineAutoSubstitutionData(params object[] values) : base(new[] { typeof(AutoNSubstituteCustomization) }, values)
{ }
}
}

View File

@ -0,0 +1,23 @@
using System;
using Xunit;
using Xunit.Sdk;
using AutoFixture.Xunit2;
using AutoFixture;
namespace Bit.Test.Common.AutoFixture.Attributes
{
public class InlineCustomAutoDataAttribute : CompositeDataAttribute
{
public InlineCustomAutoDataAttribute(Type[] iCustomizationTypes, params object[] values) : base(new DataAttribute[] {
new InlineDataAttribute(values),
new CustomAutoDataAttribute(iCustomizationTypes)
})
{ }
public InlineCustomAutoDataAttribute(ICustomization[] customizations, params object[] values) : base(new DataAttribute[] {
new InlineDataAttribute(values),
new CustomAutoDataAttribute(customizations)
})
{ }
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Linq;
using AutoFixture;
namespace Bit.Test.Common.AutoFixture.Attributes
{
public class InlineSutAutoDataAttribute : InlineCustomAutoDataAttribute
{
public InlineSutAutoDataAttribute(params object[] values) : base(
new Type[] { typeof(SutProviderCustomization) }, values)
{ }
public InlineSutAutoDataAttribute(Type[] iCustomizationTypes, params object[] values) : base(
iCustomizationTypes.Append(typeof(SutProviderCustomization)).ToArray(), values)
{ }
public InlineSutAutoDataAttribute(ICustomization[] customizations, params object[] values) : base(
customizations.Append(new SutProviderCustomization()).ToArray(), values)
{ }
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Linq;
namespace Bit.Test.Common.AutoFixture.Attributes
{
public class SutAutoDataAttribute : CustomAutoDataAttribute
{
public SutAutoDataAttribute(params Type[] iCustomizationTypes) : base(
iCustomizationTypes.Append(typeof(SutProviderCustomization)).ToArray())
{ }
}
}

View File

@ -0,0 +1,11 @@
using AutoFixture;
using AutoFixture.AutoNSubstitute;
namespace Bit.Test.Common.AutoFixture
{
public static class FixtureExtensions
{
public static IFixture WithAutoNSubstitutions(this IFixture fixture) =>
fixture.Customize(new AutoNSubstituteCustomization());
}
}

View File

@ -0,0 +1,10 @@
using System;
namespace Bit.Test.Common.AutoFixture
{
public interface ISutProvider
{
Type SutType { get; }
ISutProvider Create();
}
}

View File

@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
using AutoFixture;
using AutoFixture.Kernel;
using System.Reflection;
using System.Linq;
namespace Bit.Test.Common.AutoFixture
{
public class SutProvider<TSut> : ISutProvider
{
private Dictionary<Type, Dictionary<string, object>> _dependencies;
private readonly IFixture _fixture;
private readonly ConstructorParameterRelay<TSut> _constructorParameterRelay;
public TSut Sut { get; private set; }
public Type SutType => typeof(TSut);
public SutProvider()
{
_dependencies = new Dictionary<Type, Dictionary<string, object>>();
_fixture = new Fixture().WithAutoNSubstitutions();
_constructorParameterRelay = new ConstructorParameterRelay<TSut>(this, _fixture);
_fixture.Customizations.Add(_constructorParameterRelay);
}
public SutProvider<TSut> SetDependency<T>(T dependency, string parameterName = "") =>
SetDependency(typeof(T), dependency, parameterName);
public SutProvider<TSut> SetDependency(Type dependencyType, object dependency, string parameterName = "")
{
if (_dependencies.ContainsKey(dependencyType))
{
_dependencies[dependencyType][parameterName] = dependency;
}
else
{
_dependencies[dependencyType] = new Dictionary<string, object> { { parameterName, dependency } };
}
return this;
}
public T GetDependency<T>(string parameterName = "") => (T)GetDependency(typeof(T), parameterName);
public object GetDependency(Type dependencyType, string parameterName = "")
{
if (DependencyIsSet(dependencyType, parameterName))
{
return _dependencies[dependencyType].ContainsKey(parameterName) ? _dependencies[dependencyType][parameterName] : _dependencies[dependencyType][""];
}
else if (_dependencies.ContainsKey(dependencyType))
{
var knownDependencies = _dependencies[dependencyType];
if (knownDependencies.Values.Count == 1)
{
return _dependencies[dependencyType].Values.Single();
}
else
{
throw new ArgumentException(string.Concat($"Dependency of type {dependencyType.Name} and name ",
$"{parameterName} does not exist. Available dependency names are: ",
string.Join(", ", knownDependencies.Keys)));
}
}
else
{
throw new ArgumentException($"Dependency of type {dependencyType.Name} and name {parameterName} has not been set.");
}
}
public void Reset()
{
_dependencies = new Dictionary<Type, Dictionary<string, object>>();
Sut = default;
}
ISutProvider ISutProvider.Create() => Create();
public SutProvider<TSut> Create()
{
Sut = _fixture.Create<TSut>();
return this;
}
private bool DependencyIsSet(Type dependencyType, string parameterName = "") =>
_dependencies.ContainsKey(dependencyType) && (_dependencies[dependencyType].ContainsKey(parameterName) || _dependencies[dependencyType].ContainsKey(""));
private object GetDefault(Type type) => type.IsValueType ? Activator.CreateInstance(type) : null;
private class ConstructorParameterRelay<T> : ISpecimenBuilder
{
private readonly SutProvider<T> _sutProvider;
private readonly IFixture _fixture;
public ConstructorParameterRelay(SutProvider<T> sutProvider, IFixture fixture)
{
_sutProvider = sutProvider;
_fixture = fixture;
}
public object Create(object request, ISpecimenContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (!(request is ParameterInfo parameterInfo))
{
return new NoSpecimen();
}
if (parameterInfo.Member.DeclaringType != typeof(T) ||
parameterInfo.Member.MemberType != MemberTypes.Constructor)
{
return new NoSpecimen();
}
if (_sutProvider.DependencyIsSet(parameterInfo.ParameterType, parameterInfo.Name))
{
return _sutProvider.GetDependency(parameterInfo.ParameterType, parameterInfo.Name);
}
// This is the equivalent of _fixture.Create<parameterInfo.ParameterType>, but no overload for
// Create(Type type) exists.
var dependency = new SpecimenContext(_fixture).Resolve(new SeededRequest(parameterInfo.ParameterType,
_sutProvider.GetDefault(parameterInfo.ParameterType)));
_sutProvider.SetDependency(parameterInfo.ParameterType, dependency, parameterInfo.Name);
return dependency;
}
}
}
}

View File

@ -0,0 +1,32 @@
using System;
using AutoFixture;
using AutoFixture.Kernel;
namespace Bit.Test.Common.AutoFixture
{
public class SutProviderCustomization : ICustomization, ISpecimenBuilder
{
public object Create(object request, ISpecimenContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (!(request is Type typeRequest))
{
return new NoSpecimen();
}
if (!typeof(ISutProvider).IsAssignableFrom(typeRequest))
{
return new NoSpecimen();
}
return ((ISutProvider)Activator.CreateInstance(typeRequest)).Create();
}
public void Customize(IFixture fixture)
{
fixture.Customizations.Add(this);
}
}
}

21
test/Common/Common.csproj Normal file
View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
<RootNamespace>Bit.Test.Common</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NSubstitute" Version="4.2.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="AutoFixture.Xunit2" Version="4.14.0" />
<PackageReference Include="AutoFixture.AutoNSubstitute" Version="4.14.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
</ItemGroup>
</Project>

58
test/Common/TestHelper.cs Normal file
View File

@ -0,0 +1,58 @@
using System.Reflection;
using System.IO;
using System.Linq;
using Xunit;
using System;
using Newtonsoft.Json;
namespace Bit.Test.Common
{
public static class TestHelper
{
public static void AssertPropertyEqual(object expected, object actual, params string[] excludedPropertyStrings)
{
var relevantExcludedProperties = excludedPropertyStrings.Where(name => !name.Contains('.')).ToList();
if (expected == null)
{
Assert.Null(actual);
return;
}
if (actual == null)
{
throw new Exception("Expected object is null but actual is not");
}
foreach (var expectedPi in expected.GetType().GetProperties().Where(pi => !relevantExcludedProperties.Contains(pi.Name)))
{
var actualPi = actual.GetType().GetProperty(expectedPi.Name);
if (actualPi == null)
{
var settings = new JsonSerializerSettings { Formatting = Formatting.Indented };
throw new Exception(string.Concat($"Expected actual object to contain a property named {expectedPi.Name}, but it does not\n",
$"Expected:\n{JsonConvert.SerializeObject(expected, settings)}\n",
$"Actual:\n{JsonConvert.SerializeObject(actual, new JsonSerializerSettings { Formatting = Formatting.Indented })}"));
}
if (expectedPi.PropertyType == typeof(string) || expectedPi.PropertyType.IsValueType)
{
Assert.Equal(expectedPi.GetValue(expected), actualPi.GetValue(actual));
}
else
{
var prefix = $"{expectedPi.PropertyType.Name}.";
var nextExcludedProperties = excludedPropertyStrings.Where(name => name.StartsWith(prefix))
.Select(name => name[prefix.Length..]).ToArray();
AssertPropertyEqual(expectedPi.GetValue(expected), actualPi.GetValue(actual), nextExcludedProperties);
}
}
}
public static Predicate<T> AssertEqualExpectedPredicate<T>(T expected) => (actual) =>
{
Assert.Equal(expected, actual);
return true;
};
}
}

View File

@ -0,0 +1,16 @@
using System;
using AutoFixture;
using Bit.Core.Models.Domain;
using Bit.Core.Services;
namespace Bit.Core.Test.AutoFixture
{
public class SymmetricCryptoKeyCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
var keyMaterial = (new PclCryptoFunctionService(null)).RandomBytes(32);
fixture.Register(() => new SymmetricCryptoKey(keyMaterial));
}
}
}

View File

@ -0,0 +1,65 @@
using AutoFixture;
using Bit.Core.Models.Api;
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 Bit.Core.Enums;
namespace Bit.Core.Test.AutoFixture
{
internal class TextSendCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customize<SendData>(composer => composer
.With(c => c.Type, SendType.Text)
.With(c => c.Text, fixture.Create<SendTextData>())
.Without(c => c.File));
fixture.Customize<Send>(composer => composer
.With(c => c.Type, SendType.Text)
.With(c => c.Text, fixture.Create<SendText>())
.Without(c => c.File));
fixture.Customize<SendView>(composer => composer
.With(c => c.Type, SendType.Text)
.With(c => c.Text, fixture.Create<SendTextView>())
.Without(c => c.File));
fixture.Customize<SendRequest>(composer => composer
.With(c => c.Type, SendType.Text)
.With(c => c.Text, fixture.Create<SendTextApi>())
.Without(c => c.File));
fixture.Customize<SendResponse>(composer => composer
.With(c => c.Type, SendType.Text)
.With(c => c.Text, fixture.Create<SendTextApi>())
.Without(c => c.File));
}
}
internal class FileSendCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customize<SendData>(composer => composer
.With(c => c.Type, SendType.File)
.With(c => c.File, fixture.Create<SendFileData>())
.Without(c => c.Text));
fixture.Customize<Send>(composer => composer
.With(c => c.Type, SendType.File)
.With(c => c.File, fixture.Create<SendFile>())
.Without(c => c.Text));
fixture.Customize<SendView>(composer => composer
.With(c => c.Type, SendType.File)
.With(c => c.File, fixture.Create<SendFileView>())
.Without(c => c.Text));
fixture.Customize<SendRequest>(composer => composer
.With(c => c.Type, SendType.File)
.With(c => c.File, fixture.Create<SendFileApi>())
.Without(c => c.Text));
fixture.Customize<SendResponse>(composer => composer
.With(c => c.Type, SendType.File)
.With(c => c.File, fixture.Create<SendFileApi>())
.Without(c => c.Text));
}
}
}

View File

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
<RootNamespace>Bit.Core.Test</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="NSubstitute" Version="4.2.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="AutoFixture.Xunit2" Version="4.14.0" />
<PackageReference Include="AutoFixture.AutoNSubstitute" Version="4.14.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Core\Core.csproj" />
<ProjectReference Include="..\common\Common.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,23 @@
using Bit.Core.Models.Data;
using Bit.Core.Models.Response;
using Bit.Core.Test.AutoFixture;
using Bit.Test.Common;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.Models.Data
{
public class SendDataTests
{
[Theory]
[InlineCustomAutoData(new[] { typeof(TextSendCustomization) })]
[InlineCustomAutoData(new[] { typeof(FileSendCustomization) })]
public void SendData_FromSendResponse_Success(string userId, SendResponse response)
{
var data = new SendData(response, userId);
TestHelper.AssertPropertyEqual(response, data, "UserId");
Assert.Equal(data.UserId, userId);
}
}
}

View File

@ -0,0 +1,78 @@
using System;
using System.Linq;
using Bit.Core.Models.Data;
using Bit.Core.Models.Domain;
using Bit.Test.Common;
using System.Text;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using Bit.Core.Test.AutoFixture;
using AutoFixture.AutoNSubstitute;
namespace Bit.Core.Test.Models.Domain
{
public class SendTests
{
[Theory]
[InlineCustomAutoData(new[] { typeof(FileSendCustomization) })]
[InlineCustomAutoData(new[] { typeof(TextSendCustomization) })]
public void Send_FromSendData_Success(SendData data)
{
var send = new Send(data);
TestHelper.AssertPropertyEqual(data, send, "Name", "Notes", "Key", "SendFileData.FileName", "SendFileData.Key", "SendTextData.Text");
Assert.Equal(data.Name, send.Name?.EncryptedString);
Assert.Equal(data.Notes, send.Notes?.EncryptedString);
Assert.Equal(data.Key, send.Key?.EncryptedString);
Assert.Equal(data.Text?.Text, send.Text?.Text?.EncryptedString);
Assert.Equal(data.File?.FileName, send.File?.FileName?.EncryptedString);
}
[Theory]
[InlineCustomAutoData(new[] { typeof(AutoNSubstituteCustomization), typeof(TextSendCustomization) })]
[InlineCustomAutoData(new[] { typeof(AutoNSubstituteCustomization), typeof(FileSendCustomization) })]
public async void DecryptAsync_Success(ICryptoService cryptoService, Send send)
{
var prefix = "decrypted_";
var prefixBytes = Encoding.UTF8.GetBytes(prefix);
cryptoService.DecryptToBytesAsync(Arg.Any<CipherString>(), Arg.Any<SymmetricCryptoKey>())
.Returns(info => prefixBytes.Concat(Encoding.UTF8.GetBytes(((CipherString)info[0]).EncryptedString)).ToArray());
cryptoService.DecryptFromBytesAsync(Arg.Any<byte[]>(), Arg.Any<SymmetricCryptoKey>())
.Returns(info => prefixBytes.Concat((byte[])info[0]).ToArray());
cryptoService.DecryptToUtf8Async(Arg.Any<CipherString>(), Arg.Any<SymmetricCryptoKey>())
.Returns(info => $"{prefix}{((CipherString)info[0]).EncryptedString}");
ServiceContainer.Register("cryptoService", cryptoService);
var view = await send.DecryptAsync();
string expectedDecryptionString(CipherString encryptedString) =>
encryptedString?.EncryptedString == null ? null : $"{prefix}{encryptedString.EncryptedString}";
TestHelper.AssertPropertyEqual(send, view, "Name", "Notes", "File", "Text", "Key", "UserId");
Assert.Equal(expectedDecryptionString(send.Name), view.Name);
Assert.Equal(expectedDecryptionString(send.Notes), view.Notes);
Assert.Equal(Encoding.UTF8.GetBytes(expectedDecryptionString(send.Key)), view.Key);
switch (send.Type)
{
case SendType.File:
TestHelper.AssertPropertyEqual(send.File, view.File, "FileName");
Assert.Equal(expectedDecryptionString(send.File.FileName), view.File.FileName);
break;
case SendType.Text:
TestHelper.AssertPropertyEqual(send.Text, view?.Text, "Text");
Assert.Equal(expectedDecryptionString(send.Text.Text), view.Text.Text);
break;
default:
throw new Exception("Untested Send type");
}
ServiceContainer.Reset();
}
}
}

View File

@ -0,0 +1,43 @@
using System;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Request;
using Bit.Core.Test.AutoFixture;
using Bit.Core.Utilities;
using Bit.Test.Common;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.Models.Request
{
public class SendRequestTests
{
[Theory]
[InlineCustomAutoData(new[] { typeof(TextSendCustomization) })]
[InlineCustomAutoData(new[] { typeof(FileSendCustomization) })]
public void SendRequest_FromSend_Success(Send send)
{
var request = new SendRequest(send);
TestHelper.AssertPropertyEqual(send, request, "Id", "AccessId", "UserId", "Name", "Notes", "File", "Text", "Key", "AccessCount", "RevisionDate");
Assert.Equal(send.Name?.EncryptedString, request.Name);
Assert.Equal(send.Notes?.EncryptedString, request.Notes);
switch (send.Type)
{
case SendType.File:
// Only sets filename
Assert.Equal(send.File.FileName?.EncryptedString, request.File.FileName);
break;
case SendType.Text:
TestHelper.AssertPropertyEqual(send.Text, request?.Text, "Text");
Assert.Equal(send.Text.Text?.EncryptedString, request.Text.Text);
break;
default:
throw new Exception("Untested Send type");
}
ServiceContainer.Reset();
}
}
}

View File

@ -0,0 +1,87 @@
using System;
using System.Threading.Tasks;
using Xunit;
using Bit.Core.Services;
using Bit.Core.Enums;
using Bit.Test.Common.AutoFixture.Attributes;
using System.Text;
namespace Bit.Core.Test.Services
{
public class CryptoFunctionServiceTests
{
const string regular256Key = "qBUmEYtwTwwGPuw/z6bs/qYXXYNUlocFlyAuuANI8Pw=";
const string utf8256Key = "6DfJwW1R3txgiZKkIFTvVAb7qVlG7lKcmJGJoxR2GBU=";
const string unicode256Key = "gejGI82xthA+nKtKmIh82kjw+ttHr+ODsUoGdu5sf0A=";
const string regular512Key = "xe5cIG6ZfwGmb1FvsOedM0XKOm21myZkjL/eDeKIqqM=";
const string utf8512Key = "XQMVBnxVEhlvjSFDQc77j5GDE9aorvbS0vKnjhRg0LY=";
const string unicode512Key = "148GImrTbrjaGAe/iWEpclINM8Ehhko+9lB14+52lqc=";
const string regularSalt = "salt";
const string utf8Salt = "üser_salt";
const string unicodeSalt = "😀salt🙏";
const string regularInfo = "info";
const string utf8Info = "üser_info";
const string unicodeInfo = "😀info🙏";
const string prk16Byte = "criAmKtfzxanbgea5/kelQ==";
const string prk32Byte = "F5h4KdYQnIVH4rKH0P9CZb1GrR4n16/sJrS0PsQEn0Y=";
const string prk64Byte = "ssBK0mRG17VHdtsgt8yo4v25CRNpauH+0r2fwY/E9rLyaFBAOMbIeTry+" +
"gUJ28p8y+hFh3EI9pcrEWaNvFYonQ==";
[Theory, AutoSubstitutionData]
async public Task HkdfExpand_PrkTooSmall_Throws(PclCryptoFunctionService sut)
{
var exception = await Assert.ThrowsAsync<ArgumentException>(
() => sut.HkdfExpandAsync(Convert.FromBase64String(prk16Byte), "info", 32, HkdfAlgorithm.Sha256));
Assert.Contains("too small", exception.Message);
}
[Theory, AutoSubstitutionData]
async public Task HkdfoExpand_OutputTooBig_Throws(PclCryptoFunctionService sut)
{
var exception = await Assert.ThrowsAsync<ArgumentException>(
() => sut.HkdfExpandAsync(Convert.FromBase64String(prk32Byte), "info", 8161, HkdfAlgorithm.Sha256));
Assert.Contains("too large", exception.Message);
}
[Theory]
[InlineAutoSubstitutionData(regular256Key, HkdfAlgorithm.Sha256, prk16Byte, regularSalt, regularInfo)]
[InlineAutoSubstitutionData(utf8256Key, HkdfAlgorithm.Sha256, prk16Byte, utf8Salt, utf8Info)]
[InlineAutoSubstitutionData(unicode256Key, HkdfAlgorithm.Sha256, prk16Byte, unicodeSalt, unicodeInfo)]
[InlineAutoSubstitutionData(regular512Key, HkdfAlgorithm.Sha512, prk16Byte, regularSalt, regularInfo)]
[InlineAutoSubstitutionData(utf8512Key, HkdfAlgorithm.Sha512, prk16Byte, utf8Salt, utf8Info)]
[InlineAutoSubstitutionData(unicode512Key, HkdfAlgorithm.Sha512, prk16Byte, unicodeSalt, unicodeInfo)]
async public Task Hkdf_Success(string expectedKey, HkdfAlgorithm algorithm, string ikmString, string salt, string info, PclCryptoFunctionService sut)
{
byte[] ikm = Convert.FromBase64String(ikmString);
var key = await sut.HkdfAsync(ikm, salt, info, 32, algorithm);
Assert.Equal(expectedKey, Convert.ToBase64String(key));
var keyFromByteArray = await sut.HkdfAsync(ikm, Encoding.UTF8.GetBytes(salt), Encoding.UTF8.GetBytes(info), 32, algorithm);
Assert.Equal(key, keyFromByteArray);
}
[Theory]
[InlineAutoSubstitutionData("BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD8=",
HkdfAlgorithm.Sha256, prk32Byte, 32, regularInfo)]
[InlineAutoSubstitutionData("BnIqJlfnHm0e/2iB/15cbHyR19ARPIcWRp4oNS22CD9BV+/queOZenPNkDhmlVyL2WZ3OSU5+7ISNF5NhNfvZA==",
HkdfAlgorithm.Sha256, prk32Byte, 64, regularInfo)]
[InlineAutoSubstitutionData("uLWbMWodSBms5uGJ5WTRTesyW+MD7nlpCZvagvIRXlk=",
HkdfAlgorithm.Sha512, prk64Byte, 32, regularInfo)]
[InlineAutoSubstitutionData("uLWbMWodSBms5uGJ5WTRTesyW+MD7nlpCZvagvIRXlkY5Pv0sB+MqvaopmkC6sD/j89zDwTV9Ib2fpucUydO8w==",
HkdfAlgorithm.Sha512, prk64Byte, 64, regularInfo)]
async public Task HkdfExpand_Success(string expectedKey, HkdfAlgorithm algorithm, string prkString, int outputByteSize, string info, PclCryptoFunctionService sut)
{
var prk = Convert.FromBase64String(prkString);
var key = await sut.HkdfExpandAsync(prk, info, outputByteSize, algorithm);
Assert.Equal(expectedKey, Convert.ToBase64String(key));
var keyFromByteArray = await sut.HkdfExpandAsync(prk, Encoding.UTF8.GetBytes(info), outputByteSize, algorithm);
Assert.Equal(key, keyFromByteArray);
}
}
}

View File

@ -0,0 +1,376 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.Core.Models.Data;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Response;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Core.Enums;
using Bit.Test.Common;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Newtonsoft.Json;
using NSubstitute;
using Xunit;
using System.Text;
using System.Net.Http;
using Bit.Core.Models.Request;
using Bit.Core.Test.AutoFixture;
using System.Linq.Expressions;
using Bit.Core.Models.View;
namespace Bit.Core.Test.Services
{
public class SendServiceTests
{
private string GetSendKey(string userId) => SendService.GetSendKey(userId);
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })]
public async Task ReplaceAsync_Success(SutProvider<SendService> sutProvider, string userId, IEnumerable<SendData> sendDatas)
{
var actualSendDataDict = sendDatas.ToDictionary(d => d.Id, d => d);
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
await sutProvider.Sut.ReplaceAsync(actualSendDataDict);
await sutProvider.GetDependency<IStorageService>()
.Received(1).SaveAsync(GetSendKey(userId), actualSendDataDict);
}
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) }, 0)]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) }, 1)]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) }, 2)]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) }, 3)]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) }, 4)]
public async Task DeleteAsync_Success(int numberToDelete, SutProvider<SendService> sutProvider, string userId, IEnumerable<SendData> sendDatas)
{
var actualSendDataDict = sendDatas.ToDictionary(d => d.Id, d => d);
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
sutProvider.GetDependency<IStorageService>()
.GetAsync<Dictionary<string, SendData>>(GetSendKey(userId)).Returns(actualSendDataDict);
var idsToDelete = actualSendDataDict.Take(numberToDelete).Select(kvp => kvp.Key).ToArray();
var expectedSends = actualSendDataDict.Skip(numberToDelete).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
await sutProvider.Sut.DeleteAsync(idsToDelete);
await sutProvider.GetDependency<IStorageService>().Received(1)
.SaveAsync(GetSendKey(userId),
Arg.Is<Dictionary<string, SendData>>(s => TestHelper.AssertEqualExpectedPredicate(expectedSends)(s)));
}
[Theory, SutAutoData]
public async Task ClearAsync_Success(SutProvider<SendService> sutProvider, string userId)
{
await sutProvider.Sut.ClearAsync(userId);
await sutProvider.GetDependency<IStorageService>().Received(1).RemoveAsync(GetSendKey(userId));
}
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })]
public async Task DeleteWithServerAsync_Success(SutProvider<SendService> sutProvider, string userId, IEnumerable<SendData> sendDatas)
{
var initialSendDatas = sendDatas.ToDictionary(d => d.Id, d => d);
var idToDelete = initialSendDatas.First().Key;
var expectedSends = initialSendDatas.Skip(1).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
sutProvider.GetDependency<IStorageService>()
.GetAsync<Dictionary<string, SendData>>(Arg.Any<string>()).Returns(initialSendDatas);
await sutProvider.Sut.DeleteWithServerAsync(idToDelete);
await sutProvider.GetDependency<IApiService>().Received(1).DeleteSendAsync(idToDelete);
await sutProvider.GetDependency<IStorageService>().Received(1)
.SaveAsync(GetSendKey(userId),
Arg.Is<Dictionary<string, SendData>>(s => TestHelper.AssertEqualExpectedPredicate(expectedSends)(s)));
}
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })]
public async Task GetAsync_Success(SutProvider<SendService> sutProvider, string userId, IEnumerable<SendData> sendDatas)
{
var sendDataDict = sendDatas.ToDictionary(d => d.Id, d => d);
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
sutProvider.GetDependency<IStorageService>().GetAsync<Dictionary<string, SendData>>(GetSendKey(userId)).Returns(sendDataDict);
foreach (var dataKvp in sendDataDict)
{
var expected = new Send(dataKvp.Value);
var actual = await sutProvider.Sut.GetAsync(dataKvp.Key);
TestHelper.AssertPropertyEqual(expected, actual);
}
}
[Theory, SutAutoData]
public async Task GetAsync_NonExistringId_ReturnsNull(SutProvider<SendService> sutProvider, string userId, IEnumerable<SendData> sendDatas)
{
var sendDataDict = sendDatas.ToDictionary(d => d.Id, d => d);
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
sutProvider.GetDependency<IStorageService>().GetAsync<Dictionary<string, SendData>>(GetSendKey(userId)).Returns(sendDataDict);
var actual = await sutProvider.Sut.GetAsync(Guid.NewGuid().ToString());
Assert.Null(actual);
}
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })]
public async Task GetAllAsync_Success(SutProvider<SendService> sutProvider, string userId, IEnumerable<SendData> sendDatas)
{
var sendDataDict = sendDatas.ToDictionary(d => d.Id, d => d);
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
sutProvider.GetDependency<IStorageService>().GetAsync<Dictionary<string, SendData>>(GetSendKey(userId)).Returns(sendDataDict);
var allExpected = sendDataDict.Select(kvp => new Send(kvp.Value));
var allActual = await sutProvider.Sut.GetAllAsync();
foreach (var (actual, expected) in allActual.Zip(allExpected))
{
TestHelper.AssertPropertyEqual(expected, actual);
}
}
[Theory, SutAutoData]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })]
public async Task GetAllDecryptedAsync_Success(SutProvider<SendService> sutProvider, string userId, IEnumerable<SendData> sendDatas)
{
var sendDataDict = sendDatas.ToDictionary(d => d.Id, d => d);
sutProvider.GetDependency<ICryptoService>().HasKeyAsync().Returns(true);
ServiceContainer.Register("cryptoService", sutProvider.GetDependency<ICryptoService>());
sutProvider.GetDependency<II18nService>().StringComparer.Returns(StringComparer.CurrentCulture);
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
sutProvider.GetDependency<IStorageService>().GetAsync<Dictionary<string, SendData>>(GetSendKey(userId)).Returns(sendDataDict);
var actual = await sutProvider.Sut.GetAllDecryptedAsync();
Assert.Equal(sendDataDict.Count, actual.Count);
foreach (var (actualView, expectedId) in actual.Zip(sendDataDict.Select(s => s.Key)))
{
// Note Send -> SendView is tested in SendTests
Assert.Equal(expectedId, actualView.Id);
}
ServiceContainer.Reset();
}
// SaveWithServer()
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })]
public async Task SaveWithServerAsync_NewTextSend_Success(SutProvider<SendService> sutProvider, string userId, SendResponse response, Send send)
{
send.Id = null;
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
sutProvider.GetDependency<IApiService>().PostSendAsync(Arg.Any<SendRequest>()).Returns(response);
sutProvider.GetDependency<IApiService>().PostSendFileAsync(Arg.Any<MultipartFormDataContent>()).Returns(response);
var fileContentBytes = Encoding.UTF8.GetBytes("This is the file content");
await sutProvider.Sut.SaveWithServerAsync(send, fileContentBytes);
Predicate<SendRequest> sendRequestPredicate = r =>
{
// Note Send -> SendRequest tested in SendRequestTests
TestHelper.AssertPropertyEqual(new SendRequest(send), r);
return true;
};
switch (send.Type)
{
case SendType.Text:
await sutProvider.GetDependency<IApiService>().Received(1)
.PostSendAsync(Arg.Is<SendRequest>(r => sendRequestPredicate(r)));
break;
case SendType.File:
default:
throw new Exception("Untested send type");
}
}
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })]
public async Task SaveWithServerAsync_NewFileSend_Success(SutProvider<SendService> sutProvider, string userId, SendResponse response, Send send)
{
send.Id = null;
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
sutProvider.GetDependency<IApiService>().PostSendAsync(Arg.Any<SendRequest>()).Returns(response);
sutProvider.GetDependency<IApiService>().PostSendFileAsync(Arg.Any<MultipartFormDataContent>()).Returns(response);
var fileContentBytes = Encoding.UTF8.GetBytes("This is the file content");
await sutProvider.Sut.SaveWithServerAsync(send, fileContentBytes);
Predicate<MultipartFormDataContent> formDataPredicate = fd =>
{
Assert.Equal(2, fd.Count()); // expect a request and file content
var expectedRequest = JsonConvert.SerializeObject(new SendRequest(send));
var actualRequest = fd.First().ReadAsStringAsync().GetAwaiter().GetResult();
Assert.Equal(expectedRequest, actualRequest);
var actualFileContent = fd.Skip(1).First().ReadAsByteArrayAsync().GetAwaiter().GetResult();
Assert.Equal(fileContentBytes, actualFileContent);
return true;
};
switch (send.Type)
{
case SendType.File:
await sutProvider.GetDependency<IApiService>().Received(1)
.PostSendFileAsync(Arg.Is<MultipartFormDataContent>(f => formDataPredicate(f)));
break;
case SendType.Text:
default:
throw new Exception("Untested send type");
}
}
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })]
public async Task SaveWithServerAsync_PutSend_Success(SutProvider<SendService> sutProvider, string userId, SendResponse response, Send send)
{
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
sutProvider.GetDependency<IApiService>().PutSendAsync(send.Id, Arg.Any<SendRequest>()).Returns(response);
await sutProvider.Sut.SaveWithServerAsync(send, null);
Predicate<SendRequest> sendRequestPredicate = r =>
{
// Note Send -> SendRequest tested in SendRequestTests
TestHelper.AssertPropertyEqual(new SendRequest(send), r);
return true;
};
await sutProvider.GetDependency<IApiService>().Received(1)
.PutSendAsync(send.Id, Arg.Is<SendRequest>(r => sendRequestPredicate(r)));
}
[Theory, SutAutoData]
public async Task RemovePasswordWithServerAsync_Success(SutProvider<SendService> sutProvider, SendResponse response, string sendId)
{
sutProvider.GetDependency<IApiService>().PutSendRemovePasswordAsync(sendId).Returns(response);
await sutProvider.Sut.RemovePasswordWithServerAsync(sendId);
await sutProvider.GetDependency<IApiService>().Received(1).PutSendRemovePasswordAsync(sendId);
await sutProvider.GetDependency<IStorageService>().ReceivedWithAnyArgs(1).SaveAsync<Dictionary<string, SendData>>(default, default);
}
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })]
public async Task UpsertAsync_Update_Success(SutProvider<SendService> sutProvider, string userId, IEnumerable<SendData> initialSends)
{
var initialSendDict = initialSends.ToDictionary(s => s.Id, s => s);
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
sutProvider.GetDependency<IStorageService>().GetAsync<Dictionary<string, SendData>>(GetSendKey(userId)).Returns(initialSendDict);
var updatedSends = CoreHelpers.Clone(initialSendDict);
foreach (var kvp in updatedSends)
{
kvp.Value.Disabled = !kvp.Value.Disabled;
}
await sutProvider.Sut.UpsertAsync(updatedSends.Values.ToArray());
Predicate<Dictionary<string, SendData>> matchSendsPredicate = actual =>
{
Assert.Equal(updatedSends.Count, actual.Count);
foreach (var (expectedKvp, actualKvp) in updatedSends.Zip(actual))
{
Assert.Equal(expectedKvp.Key, actualKvp.Key);
TestHelper.AssertPropertyEqual(expectedKvp.Value, actualKvp.Value);
}
return true;
};
await sutProvider.GetDependency<IStorageService>().Received(1).SaveAsync(GetSendKey(userId), Arg.Is<Dictionary<string, SendData>>(d => matchSendsPredicate(d)));
}
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(TextSendCustomization) })]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(FileSendCustomization) })]
public async Task UpsertAsync_NewSends_Success(SutProvider<SendService> sutProvider, string userId, IEnumerable<SendData> initialSends, IEnumerable<SendData> newSends)
{
var initialSendDict = initialSends.ToDictionary(s => s.Id, s => s);
sutProvider.GetDependency<IUserService>().GetUserIdAsync().Returns(userId);
sutProvider.GetDependency<IStorageService>().GetAsync<Dictionary<string, SendData>>(GetSendKey(userId)).Returns(initialSendDict);
var expectedDict = CoreHelpers.Clone(initialSendDict).Concat(newSends.Select(s => new KeyValuePair<string, SendData>(s.Id, s)));
await sutProvider.Sut.UpsertAsync(newSends.ToArray());
Predicate<Dictionary<string, SendData>> matchSendsPredicate = actual =>
{
Assert.Equal(expectedDict.Count(), actual.Count);
foreach (var (expectedKvp, actualKvp) in expectedDict.Zip(actual))
{
Assert.Equal(expectedKvp.Key, actualKvp.Key);
TestHelper.AssertPropertyEqual(expectedKvp.Value, actualKvp.Value);
}
return true;
};
await sutProvider.GetDependency<IStorageService>().Received(1).SaveAsync(GetSendKey(userId), Arg.Is<Dictionary<string, SendData>>(d => matchSendsPredicate(d)));
}
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(SymmetricCryptoKeyCustomization), typeof(TextSendCustomization) })]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization), typeof(SymmetricCryptoKeyCustomization), typeof(FileSendCustomization) })]
public async Task EncryptAsync_Success(SutProvider<SendService> sutProvider, SendView view, byte[] fileData, SymmetricCryptoKey privateKey)
{
var prefix = "encrypted_";
var prefixBytes = Encoding.UTF8.GetBytes(prefix);
byte[] getPbkdf(string password, byte[] key) =>
prefixBytes.Concat(Encoding.UTF8.GetBytes(password)).Concat(key).ToArray();
CipherString encryptBytes(byte[] secret, SymmetricCryptoKey key) =>
new CipherString($"{prefix}{Convert.ToBase64String(secret)}{Convert.ToBase64String(key.Key)}");
CipherString encrypt(string secret, SymmetricCryptoKey key) =>
new CipherString($"{prefix}{secret}{Convert.ToBase64String(key.Key)}");
sutProvider.GetDependency<ICryptoFunctionService>().Pbkdf2Async(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<CryptoHashAlgorithm>(), Arg.Any<int>())
.Returns(info => getPbkdf((string)info[0], (byte[])info[1]));
sutProvider.GetDependency<ICryptoService>().EncryptAsync(Arg.Any<byte[]>(), Arg.Any<SymmetricCryptoKey>())
.Returns(info => encryptBytes((byte[])info[0], (SymmetricCryptoKey)info[1]));
sutProvider.GetDependency<ICryptoService>().EncryptAsync(Arg.Any<string>(), Arg.Any<SymmetricCryptoKey>())
.Returns(info => encrypt((string)info[0], (SymmetricCryptoKey)info[1]));
var (send, encryptedFileData) = await sutProvider.Sut.EncryptAsync(view, fileData, view.Password, privateKey);
TestHelper.AssertPropertyEqual(view, send, "Password", "Key", "Name", "Notes", "Text", "File",
"AccessCount", "AccessId", "CryptoKey", "RevisionDate", "DeletionDate", "ExpirationDate", "UrlB64Key",
"MaxAccessCountReached", "Expired", "PendingDelete");
Assert.Equal(Convert.ToBase64String(getPbkdf(view.Password, view.Key)), send.Password);
TestHelper.AssertPropertyEqual(encryptBytes(view.Key, privateKey), send.Key);
TestHelper.AssertPropertyEqual(encrypt(view.Name, view.CryptoKey), send.Name);
TestHelper.AssertPropertyEqual(encrypt(view.Notes, view.CryptoKey), send.Notes);
switch (view.Type)
{
case SendType.Text:
TestHelper.AssertPropertyEqual(view.Text, send.Text, "Text", "MaskedText");
TestHelper.AssertPropertyEqual(encrypt(view.Text.Text, view.CryptoKey), send.Text.Text);
break;
case SendType.File:
// Only set filename
TestHelper.AssertPropertyEqual(encrypt(view.File.FileName, view.CryptoKey), send.File.FileName);
TestHelper.AssertPropertyEqual(encryptBytes(fileData, view.CryptoKey), encryptedFileData);
break;
default:
throw new Exception("Untested send type");
}
}
}
}