// 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; }
}