// FIXME: Update this file to be null safe and then delete the line below #nullable disable using System.ComponentModel.DataAnnotations; using System.Text.Json; using Bit.Api.Tools.Utilities; using Bit.Core.Exceptions; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Services; using Bit.Core.Utilities; using static System.StringSplitOptions; namespace Bit.Api.Tools.Models.Request; /// /// A send request issued by a Bitwarden client /// public class SendRequestModel { /// /// Indicates whether the send contains text or file data. /// public SendType Type { get; set; } /// /// Specifies the authentication method required to access this Send. /// public AuthType? AuthType { get; set; } /// /// Estimated length of the file accompanying the send. when /// is . /// public long? FileLength { get; set; } = null; /// /// Label for the send. /// [EncryptedString] [EncryptedStringLength(1000)] public string Name { get; set; } /// /// Notes for the send. This is only visible to the owner of the send. /// [EncryptedString] [EncryptedStringLength(1000)] public string Notes { get; set; } /// /// A base64-encoded byte array containing the Send's encryption key. This key is /// also provided to send recipients in the Send's URL. /// [Required] [EncryptedString] [EncryptedStringLength(1000)] public string Key { get; set; } /// /// The maximum number of times a send can be accessed before it expires. /// When this value is , there is no limit. /// [Range(1, int.MaxValue)] public int? MaxAccessCount { get; set; } /// /// The date after which a send cannot be accessed. When this value is /// , there is no expiration date. /// public DateTime? ExpirationDate { get; set; } /// /// The date after which a send may be automatically deleted from the server. /// When this is , the send may be deleted after it has /// exceeded the global send timeout limit. /// [Required] public DateTime? DeletionDate { get; set; } /// /// Contains file metadata uploaded with the send. /// The file content is uploaded separately. /// public SendFileModel File { get; set; } /// /// Contains text data uploaded with the send. /// public SendTextModel Text { get; set; } /// /// Base64-encoded byte array of a password hash that grants access to the send. /// Mutually exclusive with . /// [StringLength(1000)] public string Password { get; set; } /// /// Comma-separated list of emails that may access the send using OTP /// authentication. Mutually exclusive with . /// [StringLength(4000)] public string Emails { get; set; } /// /// When , send access is disabled. /// Defaults to . /// [Required] public bool? Disabled { get; set; } /// /// When send access hides the user's email address /// and displays a confirmation message instead. Defaults to . /// public bool? HideEmail { get; set; } /// /// Transforms the request into a send object. /// /// The user that owns the send. /// Hashes the send password. /// The send object public Send ToSend(Guid userId, ISendAuthorizationService sendAuthorizationService) { var send = new Send { Type = Type, UserId = (Guid?)userId }; send = UpdateSend(send, sendAuthorizationService); return send; } /// /// Transforms the request into a send object and file data. /// /// The user that owns the send. /// Name of the file uploaded with the send. /// Hashes the send password. /// The send object and file data. public (Send, SendFileData) ToSend(Guid userId, string fileName, ISendAuthorizationService sendAuthorizationService) { // FIXME: This method does two things: creates a send and a send file data. // It should only do one thing. var send = ToSendBase(new Send { Type = Type, UserId = (Guid?)userId }, sendAuthorizationService); var data = new SendFileData(Name, Notes, fileName); return (send, data); } /// /// Update a send object with request content /// /// The send to update /// Hashes the send password. /// The send object public Send UpdateSend(Send existingSend, ISendAuthorizationService sendAuthorizationService) { existingSend = ToSendBase(existingSend, sendAuthorizationService); switch (existingSend.Type) { case SendType.File: var fileData = JsonSerializer.Deserialize(existingSend.Data); fileData.Name = Name; fileData.Notes = Notes; existingSend.Data = JsonSerializer.Serialize(fileData, JsonHelpers.IgnoreWritingNull); break; case SendType.Text: existingSend.Data = JsonSerializer.Serialize(ToSendTextData(), JsonHelpers.IgnoreWritingNull); break; default: throw new ArgumentException("Unsupported type: " + nameof(Type) + "."); } return existingSend; } /// /// Validates that the request is internally consistent for send creation. /// /// /// Thrown when the send's expiration date has already expired. /// public void ValidateCreation() { var now = DateTime.UtcNow; // Add 1 minute for a sane buffer and client clock float var nowPlus1Minute = now.AddMinutes(1); if (ExpirationDate.HasValue && ExpirationDate.Value <= nowPlus1Minute) { throw new BadRequestException("You cannot create a Send that is already expired. " + "Adjust the expiration date and try again."); } ValidateEdit(); } /// /// Validates that the request is internally consistent for send administration. /// /// /// Thrown when the send's deletion date has already expired or when its /// expiration occurs after its deletion. /// public void ValidateEdit() { var now = DateTime.UtcNow; // Add 1 minute for a sane buffer and client clock float var nowPlus1Minute = now.AddMinutes(1); if (DeletionDate.HasValue) { if (DeletionDate.Value <= nowPlus1Minute) { throw new BadRequestException("You cannot have a Send with a deletion date in the past. " + "Adjust the deletion date and try again."); } if (DeletionDate.Value > now.AddDays(31)) { throw new BadRequestException("You cannot have a Send with a deletion date that far " + "into the future. Adjust the Deletion Date to a value less than 31 days from now " + "and try again."); } } if (ExpirationDate.HasValue) { if (ExpirationDate.Value <= nowPlus1Minute) { throw new BadRequestException("You cannot have a Send with an expiration date in the past. " + "Adjust the expiration date and try again."); } if (ExpirationDate.Value > DeletionDate.Value) { throw new BadRequestException("You cannot have a Send with an expiration date greater than the deletion date. " + "Adjust the expiration date and try again."); } } } private Send ToSendBase(Send existingSend, ISendAuthorizationService authorizationService) { existingSend.Key = Key; existingSend.ExpirationDate = ExpirationDate; existingSend.DeletionDate = DeletionDate.Value; existingSend.MaxAccessCount = MaxAccessCount; if (!string.IsNullOrWhiteSpace(Emails)) { // normalize encoding var emails = Emails.Split(',', RemoveEmptyEntries | TrimEntries); existingSend.Emails = string.Join(",", emails); existingSend.Password = null; existingSend.AuthType = Core.Tools.Enums.AuthType.Email; } else if (!string.IsNullOrWhiteSpace(Password)) { existingSend.Password = authorizationService.HashPassword(Password); existingSend.Emails = null; existingSend.AuthType = Core.Tools.Enums.AuthType.Password; } else { // Neither Password nor Emails provided - preserve existing values and infer AuthType existingSend.AuthType = SendUtilities.InferAuthType(existingSend); } existingSend.Disabled = Disabled.GetValueOrDefault(); existingSend.HideEmail = HideEmail.GetValueOrDefault(); return existingSend; } private SendTextData ToSendTextData() { return new SendTextData(Name, Notes, Text.Text, Text.Hidden); } } /// /// A send request issued by a Bitwarden client /// public class SendWithIdRequestModel : SendRequestModel { /// /// Identifies the send. When this is , the client is requesting /// a new send. /// [Required] public Guid? Id { get; set; } }