Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions src/Api/Tools/Controllers/ReceivesController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
using Bit.Api.Models.Response;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Tools.Models.Response;
using Bit.Core.Auth.Identity;
using Bit.Core.Billing.Premium.Queries;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.ReceiveFeatures.Commands.Interfaces;
using Bit.Core.Tools.ReceiveFeatures.Queries.Interfaces;
using Bit.Core.Tools.Repositories;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Bit.Api.Tools.Controllers;

[Route("receives")]
public class ReceivesController : Controller
{
private readonly IReceiveRepository _receiveRepository;
private readonly IReceiveAuthorizationService _receiveAuthorizationService;
private readonly IReceiveFileStorageService _receiveFileStorageService;
private readonly IUserRepository _userRepository;
private readonly IReceiveValidationService _receiveValidationService;
private readonly IUserService _userService;
private readonly ILogger<ReceivesController> _logger;
private readonly IFeatureService _featureService;
private readonly IPushNotificationService _pushNotificationService;
private readonly ICreateReceiveCommand _createReceiveCommand;
private readonly IUpdateReceiveCommand _updateReceiveCommand;
private readonly IUploadReceiveFileCommand _uploadReceiveFileCommand;
private readonly IHasPremiumAccessQuery _hasPremiumAccessQuery;
private readonly IReceiveOwnerQuery _receiveOwnerQuery;
private readonly IGetReceiveFileDownloadQuery _getReceiveFileDownloadQuery;

public ReceivesController(
IReceiveRepository receiveRepository,
IReceiveAuthorizationService receiveAuthorizationService,
IReceiveFileStorageService receiveFileStorageService,
IReceiveValidationService receiveValidationService,
IUserService userService,
ILogger<ReceivesController> logger,
IFeatureService featureService,
IPushNotificationService pushNotificationService,
ICreateReceiveCommand createReceiveCommand,
IUpdateReceiveCommand updateReceiveCommand,
IUploadReceiveFileCommand uploadReceiveFileCommand,
IHasPremiumAccessQuery hasPremiumAccessQuery,
IReceiveOwnerQuery receiveOwnerQuery,
IUserRepository userRepository,
IGetReceiveFileDownloadQuery getReceiveFileDownloadQuery
)
{
_receiveRepository = receiveRepository;
_receiveAuthorizationService = receiveAuthorizationService;
_receiveFileStorageService = receiveFileStorageService;
_receiveValidationService = receiveValidationService;
_userService = userService;
_logger = logger;
_featureService = featureService;
_pushNotificationService = pushNotificationService;
_hasPremiumAccessQuery = hasPremiumAccessQuery;
_createReceiveCommand = createReceiveCommand;
_updateReceiveCommand = updateReceiveCommand;
_uploadReceiveFileCommand = uploadReceiveFileCommand;
_receiveOwnerQuery = receiveOwnerQuery;
_getReceiveFileDownloadQuery = getReceiveFileDownloadQuery;
_userRepository = userRepository;
}

[Authorize(Policies.Application)]
[HttpGet("{id}")]
public async Task<ReceiveResponseModel> Get(string id)
{
var receiveId = new Guid(id);
var receive = await _receiveOwnerQuery.Get(receiveId, User);
return new ReceiveResponseModel(receive);
}

[Authorize(Policies.Application)]
[HttpGet("")]
public async Task<ListResponseModel<ReceiveResponseModel>> GetAll()
{
var receives = await _receiveOwnerQuery.GetOwned(User);
var responses = receives.Select(r => new ReceiveResponseModel(r));
return new ListResponseModel<ReceiveResponseModel>(responses);
}

[Authorize(Policies.Application)]
[HttpGet("{id}/file/{fileId}")]
public async Task<ReceiveFileDownloadDataResponseModel> GetFileDownloadData(Guid id, string fileId)
{
var url = await _getReceiveFileDownloadQuery.GetDownloadUrlAsync(id, fileId, User);
return new ReceiveFileDownloadDataResponseModel { Id = fileId, Url = url };
}

[AllowAnonymous]
[HttpGet("{id}/shared")]
public async Task<SharedReceiveResponseModel> GetShared(Guid id)
{
var receive = await GetReceiveWithSecretValidationAsync(id);
var user = await _userRepository.GetByIdAsync(receive.UserId) ?? throw new NotFoundException();
return new SharedReceiveResponseModel(receive, user.Email);
}

[AllowAnonymous]
[HttpPost("{id}/file")]
public async Task<ReceiveFileUploadDataResponseModel> GetReceiveFileUploadUrl(
Guid id, [FromBody] ReceiveFileUploadRequestModel request)
{
var receive = await GetReceiveWithSecretValidationAsync(id);
var (url, fileId) = await _uploadReceiveFileCommand.GetUploadUrlAsync(
receive, request.FileName, request.FileLength, request.EncapsulatedFileContentEncryptionKey);
if (url == null)
{
throw new BadRequestException("Invalid request.");
}

return new ReceiveFileUploadDataResponseModel(url, fileId, _receiveFileStorageService.FileUploadType);
}

[AllowAnonymous]
[HttpPost("{id}/file/{fileId}/validate")]
public async Task PostFileValidation(Guid id, string fileId)
{
var receive = await GetReceiveWithSecretValidationAsync(id);

var valid = await _uploadReceiveFileCommand.ValidateFileAsync(receive, fileId);
if (!valid)
{
throw new BadRequestException("File validation failed. The uploaded file size did not match expectations.");
}
}

private async Task<Core.Tools.Entities.Receive> GetReceiveWithSecretValidationAsync(Guid id)
{
if (!Request.Headers.TryGetValue("Receive-Secret", out var secret))
{
throw new BadRequestException("Invalid request.");
}

var receive = await _receiveRepository.GetByIdAsync(id);
if (receive == null)
{
throw new NotFoundException();
}

var decodedSecret = System.Text.Encoding.UTF8.GetString(CoreHelpers.Base64UrlDecode(secret.ToString()));
if (!string.Equals(receive.Secret, decodedSecret, StringComparison.Ordinal))
{
throw new BadRequestException("Invalid request.");
}

if (!_receiveAuthorizationService.ReceiveCanBeAccessed(receive))
{
throw new NotFoundException();
}

return receive;
}

[Authorize(Policies.Application)]
[HttpPost("")]
public async Task<ReceiveResponseModel> CreateReceiveAsync([FromBody] ReceiveRequestModel request)
{
var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException("User ID not found");
var hasPremium = await _hasPremiumAccessQuery.HasPremiumAccessAsync(userId);
if (!hasPremium)
{
throw new BadRequestException("Creating a Receive requires premium");
}

var receive = await _createReceiveCommand.CreateAsync(request.ToReceive(userId));
return new ReceiveResponseModel(receive);
}

[Authorize(Policies.Application)]
[HttpPut("{id}")]
public async Task<ReceiveResponseModel> UpdateReceiveAsync([FromRoute] Guid id, [FromBody] UpdateReceiveRequestModel request)
{
var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException("User ID not found");
var hasPremium = await _hasPremiumAccessQuery.HasPremiumAccessAsync(userId);
if (!hasPremium)
{
throw new BadRequestException("Updating a receive requires premium");
}

var updatedReceive = await _updateReceiveCommand.UpdateAsync(request.ToUpdateData(id), userId);

return new ReceiveResponseModel(updatedReceive);
}
}
37 changes: 37 additions & 0 deletions src/Api/Tools/Models/ReceiveFileModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Text.Json.Serialization;
using Bit.Core.Tools.Models.Data;
using Bit.Core.Utilities;

namespace Bit.Api.Tools.Models;

public class ReceiveFileModel
{
public ReceiveFileModel() { }

public ReceiveFileModel(ReceiveFileData data)
{
Id = data.Id;
FileName = data.FileName;
Size = data.Size;
SizeName = CoreHelpers.ReadableBytesSize(data.Size);
EncapsulatedFileContentEncryptionKey = data.EncapsulatedFileContentEncryptionKey;
Validated = data.Validated;
}

public string? Id { get; set; }

[EncryptedString]
[EncryptedStringLength(1000)]
public string FileName { get; set; } = string.Empty;

[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
public long? Size { get; set; }

public string? SizeName { get; set; }

[EncryptedString]
[EncryptedStringLength(1000)]
public string EncapsulatedFileContentEncryptionKey { get; set; } = string.Empty;

public bool Validated { get; set; }
}
35 changes: 35 additions & 0 deletions src/Api/Tools/Models/Request/ReceiveFileUploadRequestModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Utilities;

namespace Bit.Api.Tools.Models.Request;

/// <summary>
/// Request model for uploading a file to a Receive.
/// Sent by the anonymous uploader alongside the Receive-Secret header.
/// </summary>
public class ReceiveFileUploadRequestModel
{
/// <summary>
/// Encrypted file name. Encrypted with the per-file content encryption key.
/// </summary>
[Required]
[EncryptedString]
[EncryptedStringLength(1000)]
public required string FileName { get; set; }

/// <summary>
/// Expected file size in bytes.
/// </summary>
[Required]
[Range(1, long.MaxValue)]
public long FileLength { get; set; }

/// <summary>
/// The per-file content encryption key, encapsulated (wrapped)
/// with the Receive's public key.
/// </summary>
[Required]
[EncryptedString]
[EncryptedStringLength(1000)]
public required string EncapsulatedFileContentEncryptionKey { get; set; }
}
67 changes: 67 additions & 0 deletions src/Api/Tools/Models/Request/ReceiveRequestModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Models.Data;
using Bit.Core.Utilities;

namespace Bit.Api.Tools.Models.Request;

/// <summary>
/// A Receive request issued by a Bitwarden client
/// </summary>
public class ReceiveRequestModel
{
/// <summary>
/// Label for the Receive.
/// </summary>
[Required]
[EncryptedString]
[EncryptedStringLength(1000)]
public required string Name { get; set; }

/// <summary>
/// The public key wrapped by the shared content encryption key (SCEK).
/// </summary>
[Required]
[EncryptedString]
[EncryptedStringLength(1000)]
public required string ScekWrappedPublicKey { get; set; }

/// <summary>
/// The shared content encryption key (SCEK) wrapped by the owners userKey.
/// </summary>
[Required]
[EncryptedString]
[EncryptedStringLength(1000)]
public required string UserKeyWrappedSharedContentEncryptionKey { get; set; }

/// <summary>
/// The private key wrapped by the owners userKey.
/// </summary>
[Required]
[EncryptedString]
public required string UserKeyWrappedPrivateKey { get; set; }

/// <summary>
/// The date this Receive becomes unavailable to potential uploaders.
/// </summary>
public DateTime ExpirationDate { get; set; }

public Receive ToReceive(Guid userId)
{
var receive = new Receive
{
UserId = userId,
Name = Name,
Data = JsonSerializer.Serialize(new ReceiveData()),
UserKeyWrappedSharedContentEncryptionKey = UserKeyWrappedSharedContentEncryptionKey,
UserKeyWrappedPrivateKey = UserKeyWrappedPrivateKey,
ScekWrappedPublicKey = ScekWrappedPublicKey,
Secret = CoreHelpers.SecureRandomString(42),
ExpirationDate = ExpirationDate
};

return receive;
}
}

27 changes: 27 additions & 0 deletions src/Api/Tools/Models/Request/UpdateReceiveRequestModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Tools.ReceiveFeatures.Models;
using Bit.Core.Utilities;


namespace Bit.Api.Tools.Models.Request;

public class UpdateReceiveRequestModel
{
/// <summary>
/// Label for the Receive.
/// </summary>
[Required]
[EncryptedString]
[EncryptedStringLength(1000)]
public required string Name { get; set; }

/// <summary>
/// The date this Receive becomes unavailable to potential uploaders.
/// </summary>
public DateTime ExpirationDate { get; set; }

public ReceiveUpdateData ToUpdateData(Guid id)
{
return new ReceiveUpdateData { Id = id, Name = Name, ExpirationDate = ExpirationDate, };
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Bit.Core.Models.Api;

namespace Bit.Api.Tools.Models.Response;

public class ReceiveFileDownloadDataResponseModel : ResponseModel
{
public string Id { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;

public ReceiveFileDownloadDataResponseModel() : base("receive-fileDownload") { }
}
Loading
Loading