1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-16 00:03:22 +00:00

Port send jslib to mobile (#1219)

* Expand Hkdf crypto functions

* Add tests for hkdf crypto functions

Took the testing infrastructure from bitwarden/server

* Move Hkdf to cryptoFunctionService

* Port changes from bitwarden/jslib#192

* Port changes from bitwarden/jslib#205

* Make Send Expiration Optional implement changes from bitwarden/jslib#242

* Bug fixes found by testing

* Test helpers

* Test conversion between model types

* Test SendService

These are mostly happy-path tests to ensure a reasonably correct
implementation

* Add run tests step to GitHub Actions

* Test send decryption

* Test Request generation from Send

* Constructor dependencies on separate lines

* Remove unused testing infrastructure

* Rename to match class name

* Move fat arrows to previous lines

* Handle exceptions in App layer

* PR review cleanups

* Throw when attempting to save an unkown Send Type

I think it's best to only throw on unknown send types here.
I don't think we want to throw whenever we encounter one since that would
do bad things like lock up Sync if clients get out of date relative to
servers. Instead, keep the client from ruining saved data by complaining
last minute that it doesn't know what it's doing.
This commit is contained in:
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

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