using System.Runtime.CompilerServices; using Bit.Core.Auth.Models.Api.Request; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; namespace Bit.Core.Test.Services; [SutProviderCustomize] public class DeviceServiceTests { [Fact] public async Task DeviceSaveShouldUpdateRevisionDateAndPushRegistration() { var deviceRepo = Substitute.For(); var pushRepo = Substitute.For(); var deviceService = new DeviceService(deviceRepo, pushRepo); var id = Guid.NewGuid(); var userId = Guid.NewGuid(); var device = new Device { Id = id, Name = "test device", Type = DeviceType.Android, UserId = userId, PushToken = "testtoken", Identifier = "testid" }; await deviceService.SaveAsync(device); Assert.True(device.RevisionDate - DateTime.UtcNow < TimeSpan.FromSeconds(1)); await pushRepo.Received().CreateOrUpdateRegistrationAsync("testtoken", id.ToString(), userId.ToString(), "testid", DeviceType.Android); } /// /// Story: A user choosed to keep trust in one of their current trusted devices, but not in another one of their /// devices. We will rotate the trust of the currently signed in device as well as the device they chose but will /// remove the trust of the device they didn't give new keys for. /// [Theory, BitAutoData] public async Task UpdateDevicesTrustAsync_Works( SutProvider sutProvider, Guid currentUserId, Device deviceOne, Device deviceTwo, Device deviceThree) { SetupOldTrust(deviceOne); SetupOldTrust(deviceTwo); SetupOldTrust(deviceThree); deviceOne.Identifier = "current_device"; sutProvider.GetDependency() .GetManyByUserIdAsync(currentUserId) .Returns(new List { deviceOne, deviceTwo, deviceThree, }); var currentDeviceModel = new DeviceKeysUpdateRequestModel { EncryptedPublicKey = "current_encrypted_public_key", EncryptedUserKey = "current_encrypted_user_key", }; var alteredDeviceModels = new List { new OtherDeviceKeysUpdateRequestModel { DeviceId = deviceTwo.Id, EncryptedPublicKey = "encrypted_public_key_two", EncryptedUserKey = "encrypted_user_key_two", }, }; await sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel, alteredDeviceModels); // Updating trust, "current" or "other" only needs to change the EncryptedPublicKey & EncryptedUserKey await sutProvider.GetDependency() .Received(1) .UpsertAsync(Arg.Is(d => d.Id == deviceOne.Id && d.EncryptedPublicKey == "current_encrypted_public_key" && d.EncryptedUserKey == "current_encrypted_user_key" && d.EncryptedPrivateKey == "old_private_deviceOne")); await sutProvider.GetDependency() .Received(1) .UpsertAsync(Arg.Is(d => d.Id == deviceTwo.Id && d.EncryptedPublicKey == "encrypted_public_key_two" && d.EncryptedUserKey == "encrypted_user_key_two" && d.EncryptedPrivateKey == "old_private_deviceTwo")); // Clearing trust should remove all key values await sutProvider.GetDependency() .Received(1) .UpsertAsync(Arg.Is(d => d.Id == deviceThree.Id && d.EncryptedPublicKey == null && d.EncryptedUserKey == null && d.EncryptedPrivateKey == null)); // Should have recieved a total of 3 calls, the ones asserted above await sutProvider.GetDependency() .Received(3) .UpsertAsync(Arg.Any()); // TODO: .NET 8: Use nameof for parameter name. static void SetupOldTrust(Device device, [CallerArgumentExpression("device")] string expression = null) { device.EncryptedPublicKey = $"old_public_{expression}"; device.EncryptedPrivateKey = $"old_private_{expression}"; device.EncryptedUserKey = $"old_user_{expression}"; } } /// /// Story: This could result from a poor implementation of this method, if they attempt add trust to a device /// that doesn't already have trust. They would have to create brand new values and for that values to be accurate /// they would technically have all the values needed to trust a device, that is why we don't consider this bad /// enough to throw but do skip it because we'd rather keep number of ways for trust to be added to the endpoint we /// already have. /// [Theory, BitAutoData] public async Task UpdateDevicesTrustAsync_DoesNotUpdateUntrustedDevices( SutProvider sutProvider, Guid currentUserId, Device deviceOne, Device deviceTwo) { deviceOne.Identifier = "current_device"; // Make deviceTwo untrusted deviceTwo.EncryptedUserKey = string.Empty; deviceTwo.EncryptedPublicKey = string.Empty; deviceTwo.EncryptedPrivateKey = string.Empty; sutProvider.GetDependency() .GetManyByUserIdAsync(currentUserId) .Returns(new List { deviceOne, deviceTwo, }); var currentDeviceModel = new DeviceKeysUpdateRequestModel { EncryptedPublicKey = "current_encrypted_public_key", EncryptedUserKey = "current_encrypted_user_key", }; var alteredDeviceModels = new List { new OtherDeviceKeysUpdateRequestModel { DeviceId = deviceTwo.Id, EncryptedPublicKey = "encrypted_public_key_two", EncryptedUserKey = "encrypted_user_key_two", }, }; await sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel, alteredDeviceModels); // Check that UpsertAsync was called for the trusted device await sutProvider.GetDependency() .Received(1) .UpsertAsync(Arg.Is(d => d.Id == deviceOne.Id && d.EncryptedPublicKey == "current_encrypted_public_key" && d.EncryptedUserKey == "current_encrypted_user_key")); // Check that UpsertAsync was not called for the untrusted device await sutProvider.GetDependency() .DidNotReceive() .UpsertAsync(Arg.Is(d => d.Id == deviceTwo.Id)); } /// /// Story: This should only happen if someone were to take the access token from a different device and try to rotate /// a device that they don't actually have. /// [Theory, BitAutoData] public async Task UpdateDevicesTrustAsync_ThrowsNotFoundException_WhenCurrentDeviceIdentifierDoesNotExist( SutProvider sutProvider, Guid currentUserId, Device deviceOne, Device deviceTwo) { deviceOne.Identifier = "some_other_device"; deviceTwo.Identifier = "another_device"; sutProvider.GetDependency() .GetManyByUserIdAsync(currentUserId) .Returns(new List { deviceOne, deviceTwo, }); var currentDeviceModel = new DeviceKeysUpdateRequestModel { EncryptedPublicKey = "current_encrypted_public_key", EncryptedUserKey = "current_encrypted_user_key", }; await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel, Enumerable.Empty())); } /// /// Story: This should only happen from a poorly implemented user of this method but important to enforce someone /// using the method correctly, a device should only be rotated intentionally and including it as both the current /// device and one of the users other device would mean they could rotate it twice and we aren't sure /// which one they would want to win out. /// [Theory, BitAutoData] public async Task UpdateDevicesTrustAsync_ThrowsBadRequestException_WhenCurrentDeviceIsIncludedInAlteredDevices( SutProvider sutProvider, Guid currentUserId, Device deviceOne, Device deviceTwo) { deviceOne.Identifier = "current_device"; sutProvider.GetDependency() .GetManyByUserIdAsync(currentUserId) .Returns(new List { deviceOne, deviceTwo, }); var currentDeviceModel = new DeviceKeysUpdateRequestModel { EncryptedPublicKey = "current_encrypted_public_key", EncryptedUserKey = "current_encrypted_user_key", }; var alteredDeviceModels = new List { new OtherDeviceKeysUpdateRequestModel { DeviceId = deviceOne.Id, // current device is included in alteredDevices EncryptedPublicKey = "encrypted_public_key_one", EncryptedUserKey = "encrypted_user_key_one", }, }; await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel, alteredDeviceModels)); } }