1
0
mirror of https://github.com/bitwarden/mobile synced 2026-01-11 04:53:52 +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,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;
}
}