1
0
mirror of https://github.com/bitwarden/mobile synced 2026-01-07 11:03:54 +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:
Matt Gibson
2021-01-25 14:27:38 -06:00
committed by GitHub
parent 9b6bf136f1
commit 8d5614cd7b
52 changed files with 2046 additions and 38 deletions

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