From 8b4836c9ecc1f2a3eb69a1476e411a9ebf50e29d Mon Sep 17 00:00:00 2001 From: Mike Amirault Date: Fri, 17 Jul 2026 14:06:31 -0400 Subject: [PATCH 1/7] [PM-39978] Add Item type Send support --- src/Api/Tools/Controllers/SendsController.cs | 2 +- .../Tools/Models/Request/SendRequestModel.cs | 10 ++++++++ .../Models/Response/SendResponseModel.cs | 25 +++++++++++++------ src/Core/Tools/Enums/SendType.cs | 3 ++- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/Api/Tools/Controllers/SendsController.cs b/src/Api/Tools/Controllers/SendsController.cs index afb537c81402..3212bd77ba76 100644 --- a/src/Api/Tools/Controllers/SendsController.cs +++ b/src/Api/Tools/Controllers/SendsController.cs @@ -1,6 +1,5 @@ using System.Text.Json; using Azure.Messaging.EventGrid; -using Bit.Api.Models.Response; using Bit.Api.Tools.Models.Request; using Bit.Api.Tools.Models.Response; using Bit.Api.Utilities; @@ -21,6 +20,7 @@ using Bit.Core.Tools.SendFeatures.Services.Interfaces; using Bit.Core.Tools.Services; using Bit.Core.Utilities; +using Bit.HttpExtensions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Api/Tools/Models/Request/SendRequestModel.cs b/src/Api/Tools/Models/Request/SendRequestModel.cs index 2063c4d4ec87..366b66a56c60 100644 --- a/src/Api/Tools/Models/Request/SendRequestModel.cs +++ b/src/Api/Tools/Models/Request/SendRequestModel.cs @@ -87,6 +87,13 @@ public class SendRequestModel /// public SendTextModel? Text { get; set; } + /// + /// Encrypted string containing secret Send data + /// + [EncryptedString] + [EncryptedStringLength(500000)] + public string? Data { get; set; } + /// /// Base64-encoded byte array of a password hash that grants access to the send. /// Mutually exclusive with . @@ -165,6 +172,9 @@ public Send UpdateSend(Send existingSend, ISendAuthorizationService sendAuthoriz case SendType.Text: existingSend.Data = JsonSerializer.Serialize(ToSendTextData(), JsonHelpers.IgnoreWritingNull); break; + case SendType.Item: + existingSend.Data = Data ?? throw new ArgumentNullException(nameof(Data), "Data is required for Sends of type Item"); + break; default: throw new ArgumentException("Unsupported type: " + nameof(Type) + "."); } diff --git a/src/Api/Tools/Models/Response/SendResponseModel.cs b/src/Api/Tools/Models/Response/SendResponseModel.cs index 31235e6290ec..2e354ea251c5 100644 --- a/src/Api/Tools/Models/Response/SendResponseModel.cs +++ b/src/Api/Tools/Models/Response/SendResponseModel.cs @@ -1,6 +1,5 @@ using System.Text.Json; using Bit.Api.Tools.Utilities; -using Bit.Core.Models.Api; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Data; @@ -12,7 +11,7 @@ namespace Bit.Api.Tools.Models.Response; /// A response issued to a Bitwarden client in response to ownership operations. /// /// -public class SendResponseModel : ResponseModel +public class SendResponseModel : HttpExtensions.ResponseModel { /// /// Instantiates a send response model @@ -47,7 +46,6 @@ public SendResponseModel(Send send) Disabled = send.Disabled; HideEmail = send.HideEmail.GetValueOrDefault(); - SendData sendData; switch (send.Type) { case SendType.File: @@ -56,7 +54,8 @@ public SendResponseModel(Send send) throw new NullReferenceException( "Send Data is required")) ?? throw new JsonException("Failed to deserialize send file data."); - sendData = fileData; + Name = fileData.Name; + Notes = fileData.Notes; File = new SendFileModel(fileData); break; case SendType.Text: @@ -65,15 +64,20 @@ public SendResponseModel(Send send) throw new NullReferenceException( "Send Data is required")) ?? throw new JsonException("Failed to deserialize send text data."); - sendData = textData; + Name = textData.Name; + Notes = textData.Notes; Text = new SendTextModel(textData); break; + case SendType.Item: + // These fields are included in send.Data, but since the entire + // object is encrypted as a blob we can't extract them here. + Name = ""; + Notes = ""; + Data = send.Data ?? throw new NullReferenceException("Send Data is required"); + break; default: throw new ArgumentException("Unsupported " + nameof(Type) + "."); } - - Name = sendData.Name; - Notes = sendData.Notes; } /// @@ -126,6 +130,11 @@ public SendResponseModel(Send send) /// public SendTextModel? Text { get; set; } + /// + /// Encrypted string containing secret Send data + /// + public string? Data { get; set; } + /// /// A base64-encoded byte array containing the Send's encryption key. /// It's also provided to send recipients in the Send's URL. diff --git a/src/Core/Tools/Enums/SendType.cs b/src/Core/Tools/Enums/SendType.cs index cd8f53292b5b..289e016d4135 100644 --- a/src/Core/Tools/Enums/SendType.cs +++ b/src/Core/Tools/Enums/SendType.cs @@ -3,5 +3,6 @@ public enum SendType : byte { Text = 0, - File = 1 + File = 1, + Item = 2 } From 5a364b62ca0ad12ca30e1d7c0bc163e045d7086f Mon Sep 17 00:00:00 2001 From: Mike Amirault Date: Mon, 20 Jul 2026 09:20:56 -0400 Subject: [PATCH 2/7] Add Item type Send handling to SendAccessResponseModel, add test coverage throughout --- .../Response/SendAccessResponseModel.cs | 15 ++- .../Tools/Controllers/SendsControllerTests.cs | 2 +- .../Models/Request/SendRequestModelTests.cs | 62 +++++++++ .../Response/SendAccessResponseModelTests.cs | 114 ++++++++++++++++ .../Models/Response/SendResponseModelTests.cs | 125 ++++++++++++++++++ 5 files changed, 313 insertions(+), 5 deletions(-) create mode 100644 test/Api.Test/Tools/Models/Response/SendAccessResponseModelTests.cs create mode 100644 test/Api.Test/Tools/Models/Response/SendResponseModelTests.cs diff --git a/src/Api/Tools/Models/Response/SendAccessResponseModel.cs b/src/Api/Tools/Models/Response/SendAccessResponseModel.cs index 447a4ea51840..d92fc5842372 100644 --- a/src/Api/Tools/Models/Response/SendAccessResponseModel.cs +++ b/src/Api/Tools/Models/Response/SendAccessResponseModel.cs @@ -34,7 +34,6 @@ public SendAccessResponseModel(Send send) Type = send.Type; AuthType = send.AuthType; - SendData sendData; switch (send.Type) { case SendType.File: @@ -43,7 +42,7 @@ public SendAccessResponseModel(Send send) throw new NullReferenceException( "Send Data is required")) ?? throw new JsonException("Failed to deserialize send file data."); - sendData = fileData; + Name = fileData.Name; File = new SendFileModel(fileData); break; case SendType.Text: @@ -52,14 +51,17 @@ public SendAccessResponseModel(Send send) throw new NullReferenceException( "Send Data is required")) ?? throw new JsonException("Failed to deserialize send text data."); - sendData = textData; + Name = textData.Name; Text = new SendTextModel(textData); break; + case SendType.Item: + Name = ""; + Data = send.Data ?? throw new NullReferenceException("Send Data is required"); + break; default: throw new ArgumentException("Unsupported " + nameof(Type) + "."); } - Name = sendData.Name; ExpirationDate = send.ExpirationDate; } @@ -101,6 +103,11 @@ public SendAccessResponseModel(Send send) /// public SendTextModel? Text { get; set; } + /// + /// Encrypted string containing secret Send data + /// + public string? Data { get; set; } + /// /// The date after which a send cannot be accessed. When this value is /// , there is no expiration date. diff --git a/test/Api.Test/Tools/Controllers/SendsControllerTests.cs b/test/Api.Test/Tools/Controllers/SendsControllerTests.cs index 313be7a2ca76..945287c291cb 100644 --- a/test/Api.Test/Tools/Controllers/SendsControllerTests.cs +++ b/test/Api.Test/Tools/Controllers/SendsControllerTests.cs @@ -2,7 +2,6 @@ using System.Text; using System.Text.Json; using AutoFixture.Xunit2; -using Bit.Api.Models.Response; using Bit.Api.Tools.Controllers; using Bit.Api.Tools.Models; using Bit.Api.Tools.Models.Request; @@ -24,6 +23,7 @@ using Bit.Core.Tools.SendFeatures.Services.Interfaces; using Bit.Core.Tools.Services; using Bit.Core.Utilities; +using Bit.HttpExtensions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; diff --git a/test/Api.Test/Tools/Models/Request/SendRequestModelTests.cs b/test/Api.Test/Tools/Models/Request/SendRequestModelTests.cs index 144729119ce9..b32b8e9243d7 100644 --- a/test/Api.Test/Tools/Models/Request/SendRequestModelTests.cs +++ b/test/Api.Test/Tools/Models/Request/SendRequestModelTests.cs @@ -59,6 +59,68 @@ public void ToSend_Text_Success() Assert.Equal("encrypted_name", name); } + [Fact] + public void ToSend_Item_Success() + { + var deletionDate = DateTime.UtcNow.AddDays(5); + var sendRequest = new SendRequestModel + { + AuthType = AuthType.Password, + DeletionDate = deletionDate, + Disabled = false, + ExpirationDate = null, + HideEmail = false, + Key = "encrypted_key", + MaxAccessCount = null, + Name = "encrypted_name", + Notes = null, + Password = "Password", + Data = "encrypted_data", + Type = SendType.Item, + }; + + var sendAuthorizationService = Substitute.For(); + sendAuthorizationService.HashPassword(Arg.Any()) + .Returns((info) => $"hashed_{(string)info[0]}"); + + var send = sendRequest.ToSend(Guid.NewGuid(), sendAuthorizationService); + + Assert.Equal(deletionDate, send.DeletionDate); + Assert.False(send.Disabled); + Assert.Null(send.ExpirationDate); + Assert.False(send.HideEmail); + Assert.Equal("encrypted_key", send.Key); + Assert.Equal("hashed_Password", send.Password); + Assert.Equal("encrypted_data", send.Data); + } + + [Fact] + public void ToSend_Item_NullData() + { + var deletionDate = DateTime.UtcNow.AddDays(5); + var sendRequest = new SendRequestModel + { + AuthType = AuthType.Password, + DeletionDate = deletionDate, + Disabled = false, + ExpirationDate = null, + HideEmail = false, + Key = "encrypted_key", + MaxAccessCount = null, + Name = "encrypted_name", + Notes = null, + Password = "Password", + Data = null, + Type = SendType.Item, + }; + + var sendAuthorizationService = Substitute.For(); + sendAuthorizationService.HashPassword(Arg.Any()) + .Returns((info) => $"hashed_{(string)info[0]}"); + + Assert.Throws(() => sendRequest.ToSend(Guid.NewGuid(), sendAuthorizationService)); + } + [Fact] public void ValidateEdit_DeletionDateInPast_ThrowsBadRequestException() { diff --git a/test/Api.Test/Tools/Models/Response/SendAccessResponseModelTests.cs b/test/Api.Test/Tools/Models/Response/SendAccessResponseModelTests.cs new file mode 100644 index 000000000000..ca575143f896 --- /dev/null +++ b/test/Api.Test/Tools/Models/Response/SendAccessResponseModelTests.cs @@ -0,0 +1,114 @@ +using System.Text.Json; +using Bit.Api.Tools.Models.Response; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Data; +using Xunit; + +namespace Bit.Api.Test.Models.Response; + +public class SendAccessResponseModelTests +{ + [Fact] + public void NullSend_Throws() + { + Send send = null; + SendAccessResponseModel responseModel; + Assert.Throws(() => responseModel = new SendAccessResponseModel(send)); + } + + [Fact] + public void TextSend_NullData_Throws() + { + var send = new Send + { + Type = SendType.Text, + Data = null + }; + SendAccessResponseModel responseModel; + Assert.Throws(() => responseModel = new SendAccessResponseModel(send)); + } + + [Fact] + public void TextSend_NonDeserializableData_Throws() + { + var send = new Send + { + Type = SendType.Text, + Data = "bad_data" + }; + SendAccessResponseModel responseModel; + Assert.Throws(() => responseModel = new SendAccessResponseModel(send)); + } + + [Fact] + public void FileSend_NullData_Throws() + { + var send = new Send + { + Type = SendType.File, + Data = null + }; + SendAccessResponseModel responseModel; + Assert.Throws(() => responseModel = new SendAccessResponseModel(send)); + } + + [Fact] + public void FileSend_NonDeserializableData_Throws() + { + var send = new Send + { + Type = SendType.File, + Data = "bad_data" + }; + SendAccessResponseModel responseModel; + Assert.Throws(() => responseModel = new SendAccessResponseModel(send)); + } + + [Fact] + public void ItemSend_NullData_Throws() + { + var send = new Send + { + Type = SendType.Item, + Data = null + }; + SendAccessResponseModel responseModel; + Assert.Throws(() => responseModel = new SendAccessResponseModel(send)); + } + + [Fact] + public void FromSend_Success() + { + var send = new Send + { + Id = Guid.NewGuid(), + AuthType = AuthType.None, + Key = "encrypted_key", + MaxAccessCount = 5, + AccessCount = 0, + RevisionDate = new DateTime(), + ExpirationDate = new DateTime(), + DeletionDate = new DateTime(), + Password = null, + Emails = null, + Disabled = false, + HideEmail = false, + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData + { + Hidden = false, + Name = "encrypted_name", + Notes = null, + Text = "encrypted_text" + }) + }; + var responseModel = new SendAccessResponseModel(send); + Assert.Equal(send.Type, responseModel.Type); + Assert.Equal(send.AuthType, responseModel.AuthType); + Assert.Equal("encrypted_name", responseModel.Name); + Assert.Equal("encrypted_text", responseModel.Text.Text); + Assert.False(responseModel.Text.Hidden); + Assert.Equal(send.ExpirationDate, responseModel.ExpirationDate); + } +} \ No newline at end of file diff --git a/test/Api.Test/Tools/Models/Response/SendResponseModelTests.cs b/test/Api.Test/Tools/Models/Response/SendResponseModelTests.cs new file mode 100644 index 000000000000..cc6e68909db2 --- /dev/null +++ b/test/Api.Test/Tools/Models/Response/SendResponseModelTests.cs @@ -0,0 +1,125 @@ +using System.Text.Json; +using Bit.Api.Tools.Models.Response; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Data; +using Xunit; + +namespace Bit.Api.Test.Models.Response; + +public class SendResponseModelTests +{ + [Fact] + public void NullSend_Throws() + { + Send send = null; + SendResponseModel responseModel; + Assert.Throws(() => responseModel = new SendResponseModel(send)); + } + + [Fact] + public void TextSend_NullData_Throws() + { + var send = new Send + { + Type = SendType.Text, + Data = null + }; + SendResponseModel responseModel; + Assert.Throws(() => responseModel = new SendResponseModel(send)); + } + + [Fact] + public void TextSend_NonDeserializableData_Throws() + { + var send = new Send + { + Type = SendType.Text, + Data = "bad_data" + }; + SendResponseModel responseModel; + Assert.Throws(() => responseModel = new SendResponseModel(send)); + } + + [Fact] + public void FileSend_NullData_Throws() + { + var send = new Send + { + Type = SendType.File, + Data = null + }; + SendResponseModel responseModel; + Assert.Throws(() => responseModel = new SendResponseModel(send)); + } + + [Fact] + public void FileSend_NonDeserializableData_Throws() + { + var send = new Send + { + Type = SendType.File, + Data = "bad_data" + }; + SendResponseModel responseModel; + Assert.Throws(() => responseModel = new SendResponseModel(send)); + } + + [Fact] + public void ItemSend_NullData_Throws() + { + var send = new Send + { + Type = SendType.Item, + Data = null + }; + SendResponseModel responseModel; + Assert.Throws(() => responseModel = new SendResponseModel(send)); + } + + [Fact] + public void FromSend_Success() + { + var send = new Send + { + Id = Guid.NewGuid(), + AuthType = AuthType.None, + Key = "encrypted_key", + MaxAccessCount = 5, + AccessCount = 0, + RevisionDate = new DateTime(), + ExpirationDate = new DateTime(), + DeletionDate = new DateTime(), + Password = null, + Emails = null, + Disabled = false, + HideEmail = false, + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData + { + Hidden = false, + Name = "encrypted_name", + Notes = null, + Text = "encrypted_text" + }) + }; + var responseModel = new SendResponseModel(send); + Assert.Equal(send.Id, responseModel.Id); + Assert.Equal(send.AuthType, responseModel.AuthType); + Assert.Equal(send.Key, responseModel.Key); + Assert.Equal(send.MaxAccessCount, responseModel.MaxAccessCount); + Assert.Equal(send.AccessCount, responseModel.AccessCount); + Assert.Equal(send.RevisionDate, responseModel.RevisionDate); + Assert.Equal(send.ExpirationDate, responseModel.ExpirationDate); + Assert.Equal(send.DeletionDate, responseModel.DeletionDate); + Assert.Equal(send.Password, responseModel.Password); + Assert.Equal(send.Emails, responseModel.Emails); + Assert.Equal(send.Disabled, responseModel.Disabled); + Assert.Equal(send.HideEmail, responseModel.HideEmail); + Assert.Equal(send.Type, responseModel.Type); + Assert.False(responseModel.Text.Hidden); + Assert.Equal("encrypted_name", responseModel.Name); + Assert.Null(responseModel.Notes); + Assert.Equal("encrypted_text", responseModel.Text.Text); + } +} \ No newline at end of file From 480357d9c20c80d1746f3aef733f6b8b332b6302 Mon Sep 17 00:00:00 2001 From: Mike Amirault Date: Mon, 20 Jul 2026 09:24:14 -0400 Subject: [PATCH 3/7] Formatting --- .../Response/SendAccessResponseModelTests.cs | 138 +++++++-------- .../Models/Response/SendResponseModelTests.cs | 160 +++++++++--------- 2 files changed, 149 insertions(+), 149 deletions(-) diff --git a/test/Api.Test/Tools/Models/Response/SendAccessResponseModelTests.cs b/test/Api.Test/Tools/Models/Response/SendAccessResponseModelTests.cs index ca575143f896..316cc209ffdd 100644 --- a/test/Api.Test/Tools/Models/Response/SendAccessResponseModelTests.cs +++ b/test/Api.Test/Tools/Models/Response/SendAccessResponseModelTests.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using Bit.Api.Tools.Models.Response; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; @@ -12,103 +12,103 @@ public class SendAccessResponseModelTests [Fact] public void NullSend_Throws() { - Send send = null; - SendAccessResponseModel responseModel; - Assert.Throws(() => responseModel = new SendAccessResponseModel(send)); + Send send = null; + SendAccessResponseModel responseModel; + Assert.Throws(() => responseModel = new SendAccessResponseModel(send)); } [Fact] public void TextSend_NullData_Throws() { - var send = new Send - { - Type = SendType.Text, - Data = null - }; - SendAccessResponseModel responseModel; - Assert.Throws(() => responseModel = new SendAccessResponseModel(send)); + var send = new Send + { + Type = SendType.Text, + Data = null + }; + SendAccessResponseModel responseModel; + Assert.Throws(() => responseModel = new SendAccessResponseModel(send)); } [Fact] public void TextSend_NonDeserializableData_Throws() { - var send = new Send - { - Type = SendType.Text, - Data = "bad_data" - }; - SendAccessResponseModel responseModel; - Assert.Throws(() => responseModel = new SendAccessResponseModel(send)); + var send = new Send + { + Type = SendType.Text, + Data = "bad_data" + }; + SendAccessResponseModel responseModel; + Assert.Throws(() => responseModel = new SendAccessResponseModel(send)); } [Fact] public void FileSend_NullData_Throws() { - var send = new Send - { - Type = SendType.File, - Data = null - }; - SendAccessResponseModel responseModel; - Assert.Throws(() => responseModel = new SendAccessResponseModel(send)); + var send = new Send + { + Type = SendType.File, + Data = null + }; + SendAccessResponseModel responseModel; + Assert.Throws(() => responseModel = new SendAccessResponseModel(send)); } [Fact] public void FileSend_NonDeserializableData_Throws() { - var send = new Send - { - Type = SendType.File, - Data = "bad_data" - }; - SendAccessResponseModel responseModel; - Assert.Throws(() => responseModel = new SendAccessResponseModel(send)); + var send = new Send + { + Type = SendType.File, + Data = "bad_data" + }; + SendAccessResponseModel responseModel; + Assert.Throws(() => responseModel = new SendAccessResponseModel(send)); } [Fact] public void ItemSend_NullData_Throws() { - var send = new Send - { - Type = SendType.Item, - Data = null - }; - SendAccessResponseModel responseModel; - Assert.Throws(() => responseModel = new SendAccessResponseModel(send)); + var send = new Send + { + Type = SendType.Item, + Data = null + }; + SendAccessResponseModel responseModel; + Assert.Throws(() => responseModel = new SendAccessResponseModel(send)); } [Fact] public void FromSend_Success() { - var send = new Send - { - Id = Guid.NewGuid(), - AuthType = AuthType.None, - Key = "encrypted_key", - MaxAccessCount = 5, - AccessCount = 0, - RevisionDate = new DateTime(), - ExpirationDate = new DateTime(), - DeletionDate = new DateTime(), - Password = null, - Emails = null, - Disabled = false, - HideEmail = false, - Type = SendType.Text, - Data = JsonSerializer.Serialize(new SendTextData + var send = new Send { - Hidden = false, - Name = "encrypted_name", - Notes = null, - Text = "encrypted_text" - }) - }; - var responseModel = new SendAccessResponseModel(send); - Assert.Equal(send.Type, responseModel.Type); - Assert.Equal(send.AuthType, responseModel.AuthType); - Assert.Equal("encrypted_name", responseModel.Name); - Assert.Equal("encrypted_text", responseModel.Text.Text); - Assert.False(responseModel.Text.Hidden); - Assert.Equal(send.ExpirationDate, responseModel.ExpirationDate); + Id = Guid.NewGuid(), + AuthType = AuthType.None, + Key = "encrypted_key", + MaxAccessCount = 5, + AccessCount = 0, + RevisionDate = new DateTime(), + ExpirationDate = new DateTime(), + DeletionDate = new DateTime(), + Password = null, + Emails = null, + Disabled = false, + HideEmail = false, + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData + { + Hidden = false, + Name = "encrypted_name", + Notes = null, + Text = "encrypted_text" + }) + }; + var responseModel = new SendAccessResponseModel(send); + Assert.Equal(send.Type, responseModel.Type); + Assert.Equal(send.AuthType, responseModel.AuthType); + Assert.Equal("encrypted_name", responseModel.Name); + Assert.Equal("encrypted_text", responseModel.Text.Text); + Assert.False(responseModel.Text.Hidden); + Assert.Equal(send.ExpirationDate, responseModel.ExpirationDate); } -} \ No newline at end of file +} diff --git a/test/Api.Test/Tools/Models/Response/SendResponseModelTests.cs b/test/Api.Test/Tools/Models/Response/SendResponseModelTests.cs index cc6e68909db2..3b65619b3490 100644 --- a/test/Api.Test/Tools/Models/Response/SendResponseModelTests.cs +++ b/test/Api.Test/Tools/Models/Response/SendResponseModelTests.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using Bit.Api.Tools.Models.Response; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; @@ -12,114 +12,114 @@ public class SendResponseModelTests [Fact] public void NullSend_Throws() { - Send send = null; - SendResponseModel responseModel; - Assert.Throws(() => responseModel = new SendResponseModel(send)); + Send send = null; + SendResponseModel responseModel; + Assert.Throws(() => responseModel = new SendResponseModel(send)); } [Fact] public void TextSend_NullData_Throws() { - var send = new Send - { - Type = SendType.Text, - Data = null - }; - SendResponseModel responseModel; - Assert.Throws(() => responseModel = new SendResponseModel(send)); + var send = new Send + { + Type = SendType.Text, + Data = null + }; + SendResponseModel responseModel; + Assert.Throws(() => responseModel = new SendResponseModel(send)); } [Fact] public void TextSend_NonDeserializableData_Throws() { - var send = new Send - { - Type = SendType.Text, - Data = "bad_data" - }; - SendResponseModel responseModel; - Assert.Throws(() => responseModel = new SendResponseModel(send)); + var send = new Send + { + Type = SendType.Text, + Data = "bad_data" + }; + SendResponseModel responseModel; + Assert.Throws(() => responseModel = new SendResponseModel(send)); } [Fact] public void FileSend_NullData_Throws() { - var send = new Send - { - Type = SendType.File, - Data = null - }; - SendResponseModel responseModel; - Assert.Throws(() => responseModel = new SendResponseModel(send)); + var send = new Send + { + Type = SendType.File, + Data = null + }; + SendResponseModel responseModel; + Assert.Throws(() => responseModel = new SendResponseModel(send)); } [Fact] public void FileSend_NonDeserializableData_Throws() { - var send = new Send - { - Type = SendType.File, - Data = "bad_data" - }; - SendResponseModel responseModel; - Assert.Throws(() => responseModel = new SendResponseModel(send)); + var send = new Send + { + Type = SendType.File, + Data = "bad_data" + }; + SendResponseModel responseModel; + Assert.Throws(() => responseModel = new SendResponseModel(send)); } [Fact] public void ItemSend_NullData_Throws() { - var send = new Send - { - Type = SendType.Item, - Data = null - }; - SendResponseModel responseModel; - Assert.Throws(() => responseModel = new SendResponseModel(send)); + var send = new Send + { + Type = SendType.Item, + Data = null + }; + SendResponseModel responseModel; + Assert.Throws(() => responseModel = new SendResponseModel(send)); } [Fact] public void FromSend_Success() { - var send = new Send - { - Id = Guid.NewGuid(), - AuthType = AuthType.None, - Key = "encrypted_key", - MaxAccessCount = 5, - AccessCount = 0, - RevisionDate = new DateTime(), - ExpirationDate = new DateTime(), - DeletionDate = new DateTime(), - Password = null, - Emails = null, - Disabled = false, - HideEmail = false, - Type = SendType.Text, - Data = JsonSerializer.Serialize(new SendTextData + var send = new Send { - Hidden = false, - Name = "encrypted_name", - Notes = null, - Text = "encrypted_text" - }) - }; - var responseModel = new SendResponseModel(send); - Assert.Equal(send.Id, responseModel.Id); - Assert.Equal(send.AuthType, responseModel.AuthType); - Assert.Equal(send.Key, responseModel.Key); - Assert.Equal(send.MaxAccessCount, responseModel.MaxAccessCount); - Assert.Equal(send.AccessCount, responseModel.AccessCount); - Assert.Equal(send.RevisionDate, responseModel.RevisionDate); - Assert.Equal(send.ExpirationDate, responseModel.ExpirationDate); - Assert.Equal(send.DeletionDate, responseModel.DeletionDate); - Assert.Equal(send.Password, responseModel.Password); - Assert.Equal(send.Emails, responseModel.Emails); - Assert.Equal(send.Disabled, responseModel.Disabled); - Assert.Equal(send.HideEmail, responseModel.HideEmail); - Assert.Equal(send.Type, responseModel.Type); - Assert.False(responseModel.Text.Hidden); - Assert.Equal("encrypted_name", responseModel.Name); - Assert.Null(responseModel.Notes); - Assert.Equal("encrypted_text", responseModel.Text.Text); + Id = Guid.NewGuid(), + AuthType = AuthType.None, + Key = "encrypted_key", + MaxAccessCount = 5, + AccessCount = 0, + RevisionDate = new DateTime(), + ExpirationDate = new DateTime(), + DeletionDate = new DateTime(), + Password = null, + Emails = null, + Disabled = false, + HideEmail = false, + Type = SendType.Text, + Data = JsonSerializer.Serialize(new SendTextData + { + Hidden = false, + Name = "encrypted_name", + Notes = null, + Text = "encrypted_text" + }) + }; + var responseModel = new SendResponseModel(send); + Assert.Equal(send.Id, responseModel.Id); + Assert.Equal(send.AuthType, responseModel.AuthType); + Assert.Equal(send.Key, responseModel.Key); + Assert.Equal(send.MaxAccessCount, responseModel.MaxAccessCount); + Assert.Equal(send.AccessCount, responseModel.AccessCount); + Assert.Equal(send.RevisionDate, responseModel.RevisionDate); + Assert.Equal(send.ExpirationDate, responseModel.ExpirationDate); + Assert.Equal(send.DeletionDate, responseModel.DeletionDate); + Assert.Equal(send.Password, responseModel.Password); + Assert.Equal(send.Emails, responseModel.Emails); + Assert.Equal(send.Disabled, responseModel.Disabled); + Assert.Equal(send.HideEmail, responseModel.HideEmail); + Assert.Equal(send.Type, responseModel.Type); + Assert.False(responseModel.Text.Hidden); + Assert.Equal("encrypted_name", responseModel.Name); + Assert.Null(responseModel.Notes); + Assert.Equal("encrypted_text", responseModel.Text.Text); } -} \ No newline at end of file +} From 2f5633ccb95b703c560d32361dcd994c07ad2e25 Mon Sep 17 00:00:00 2001 From: Mike Amirault Date: Mon, 20 Jul 2026 15:26:53 -0400 Subject: [PATCH 4/7] Use string.Empty instead of quotes --- src/Api/Tools/Models/Response/SendAccessResponseModel.cs | 2 +- src/Api/Tools/Models/Response/SendResponseModel.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Api/Tools/Models/Response/SendAccessResponseModel.cs b/src/Api/Tools/Models/Response/SendAccessResponseModel.cs index d92fc5842372..d35cee7f2555 100644 --- a/src/Api/Tools/Models/Response/SendAccessResponseModel.cs +++ b/src/Api/Tools/Models/Response/SendAccessResponseModel.cs @@ -55,7 +55,7 @@ public SendAccessResponseModel(Send send) Text = new SendTextModel(textData); break; case SendType.Item: - Name = ""; + Name = string.Empty; Data = send.Data ?? throw new NullReferenceException("Send Data is required"); break; default: diff --git a/src/Api/Tools/Models/Response/SendResponseModel.cs b/src/Api/Tools/Models/Response/SendResponseModel.cs index 2e354ea251c5..0196b04e37ea 100644 --- a/src/Api/Tools/Models/Response/SendResponseModel.cs +++ b/src/Api/Tools/Models/Response/SendResponseModel.cs @@ -71,8 +71,8 @@ public SendResponseModel(Send send) case SendType.Item: // These fields are included in send.Data, but since the entire // object is encrypted as a blob we can't extract them here. - Name = ""; - Notes = ""; + Name = string.Empty; + Notes = string.Empty; Data = send.Data ?? throw new NullReferenceException("Send Data is required"); break; default: From bfd7ee7f3d4148b491172d43d3de4b688f3ad850 Mon Sep 17 00:00:00 2001 From: Mike Amirault Date: Thu, 23 Jul 2026 10:42:05 -0400 Subject: [PATCH 5/7] Change Send request data to string to support field by field encryption --- src/Api/Tools/Models/Request/SendRequestModel.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Api/Tools/Models/Request/SendRequestModel.cs b/src/Api/Tools/Models/Request/SendRequestModel.cs index 366b66a56c60..dd2e0621fd30 100644 --- a/src/Api/Tools/Models/Request/SendRequestModel.cs +++ b/src/Api/Tools/Models/Request/SendRequestModel.cs @@ -90,8 +90,7 @@ public class SendRequestModel /// /// Encrypted string containing secret Send data /// - [EncryptedString] - [EncryptedStringLength(500000)] + [StringLength(500000)] public string? Data { get; set; } /// From 1f9e9276b0664ab37ae03c5f9ac9551a40a25233 Mon Sep 17 00:00:00 2001 From: Mike Amirault Date: Thu, 23 Jul 2026 10:47:53 -0400 Subject: [PATCH 6/7] Correct access count logic for Item Sends --- src/Api/Tools/Controllers/SendsController.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Api/Tools/Controllers/SendsController.cs b/src/Api/Tools/Controllers/SendsController.cs index 3212bd77ba76..86d8491405ec 100644 --- a/src/Api/Tools/Controllers/SendsController.cs +++ b/src/Api/Tools/Controllers/SendsController.cs @@ -298,13 +298,13 @@ public async Task AccessUsingAuth() } /* - * AccessCount is incremented differently for File and Text Send types: - * - Text Sends are incremented at every access + * AccessCount is incremented differently depending on Send type: + * - Text and Item Sends are incremented at every access * - File Sends are incremented only when the file is downloaded * * Note that this endpoint is initially called for all Send types */ - if (send.Type == SendType.Text) + if (send.Type == SendType.Text || send.Type == SendType.Item) { send.AccessCount++; await _sendRepository.ReplaceAsync(send); From d63cef3ec38cabeb9338a648e9eba1ff9892e841 Mon Sep 17 00:00:00 2001 From: Mike Amirault Date: Thu, 23 Jul 2026 10:54:56 -0400 Subject: [PATCH 7/7] Update src/Api/Tools/Models/Request/SendRequestModel.cs Co-authored-by: John Harrington <84741727+harr1424@users.noreply.github.com> --- src/Api/Tools/Models/Request/SendRequestModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Api/Tools/Models/Request/SendRequestModel.cs b/src/Api/Tools/Models/Request/SendRequestModel.cs index dd2e0621fd30..1ea5ea55e4b7 100644 --- a/src/Api/Tools/Models/Request/SendRequestModel.cs +++ b/src/Api/Tools/Models/Request/SendRequestModel.cs @@ -88,7 +88,7 @@ public class SendRequestModel public SendTextModel? Text { get; set; } /// - /// Encrypted string containing secret Send data + /// String containing secret Send data /// [StringLength(500000)] public string? Data { get; set; }