You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
311 lines
13 KiB
311 lines
13 KiB
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.Models.Data.Organizations.OrganizationUsers; |
|
using Bit.Core.NotificationHub; |
|
using Bit.Core.Platform.Push; |
|
using Bit.Core.Repositories; |
|
using Bit.Core.Services; |
|
using Bit.Core.Settings; |
|
using Bit.Test.Common.AutoFixture; |
|
using Bit.Test.Common.AutoFixture.Attributes; |
|
using NSubstitute; |
|
using Xunit; |
|
|
|
namespace Bit.Core.Test.Services; |
|
|
|
[SutProviderCustomize] |
|
public class DeviceServiceTests |
|
{ |
|
[Theory] |
|
[BitAutoData] |
|
public async Task SaveAsync_IdProvided_UpdatedRevisionDateAndPushRegistration(Guid id, Guid userId, |
|
Guid organizationId1, Guid organizationId2, Guid installationId, |
|
OrganizationUserOrganizationDetails organizationUserOrganizationDetails1, |
|
OrganizationUserOrganizationDetails organizationUserOrganizationDetails2) |
|
{ |
|
organizationUserOrganizationDetails1.OrganizationId = organizationId1; |
|
organizationUserOrganizationDetails2.OrganizationId = organizationId2; |
|
|
|
var deviceRepo = Substitute.For<IDeviceRepository>(); |
|
var pushRepo = Substitute.For<IPushRegistrationService>(); |
|
var organizationUserRepository = Substitute.For<IOrganizationUserRepository>(); |
|
organizationUserRepository.GetManyDetailsByUserAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType?>()) |
|
.Returns([organizationUserOrganizationDetails1, organizationUserOrganizationDetails2]); |
|
var globalSettings = Substitute.For<IGlobalSettings>(); |
|
globalSettings.Installation.Id.Returns(installationId); |
|
var deviceService = new DeviceService(deviceRepo, pushRepo, organizationUserRepository, globalSettings); |
|
|
|
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(1).CreateOrUpdateRegistrationAsync(Arg.Is<PushRegistrationData>(v => v.Token == "testToken"), id.ToString(), |
|
userId.ToString(), "testid", DeviceType.Android, |
|
Arg.Do<IEnumerable<string>>(organizationIds => |
|
{ |
|
var organizationIdsList = organizationIds.ToList(); |
|
Assert.Equal(2, organizationIdsList.Count); |
|
Assert.Contains(organizationId1.ToString(), organizationIdsList); |
|
Assert.Contains(organizationId2.ToString(), organizationIdsList); |
|
}), installationId); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task SaveAsync_IdNotProvided_CreatedAndPushRegistration(Guid userId, Guid organizationId1, |
|
Guid organizationId2, Guid installationId, |
|
OrganizationUserOrganizationDetails organizationUserOrganizationDetails1, |
|
OrganizationUserOrganizationDetails organizationUserOrganizationDetails2) |
|
{ |
|
organizationUserOrganizationDetails1.OrganizationId = organizationId1; |
|
organizationUserOrganizationDetails2.OrganizationId = organizationId2; |
|
|
|
var deviceRepo = Substitute.For<IDeviceRepository>(); |
|
var pushRepo = Substitute.For<IPushRegistrationService>(); |
|
var organizationUserRepository = Substitute.For<IOrganizationUserRepository>(); |
|
organizationUserRepository.GetManyDetailsByUserAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType?>()) |
|
.Returns([organizationUserOrganizationDetails1, organizationUserOrganizationDetails2]); |
|
var globalSettings = Substitute.For<IGlobalSettings>(); |
|
globalSettings.Installation.Id.Returns(installationId); |
|
var deviceService = new DeviceService(deviceRepo, pushRepo, organizationUserRepository, globalSettings); |
|
|
|
var device = new Device |
|
{ |
|
Name = "test device", |
|
Type = DeviceType.Android, |
|
UserId = userId, |
|
PushToken = "testToken", |
|
Identifier = "testid" |
|
}; |
|
await deviceService.SaveAsync(device); |
|
|
|
await pushRepo.Received(1).CreateOrUpdateRegistrationAsync(Arg.Is<PushRegistrationData>(v => v.Token == "testToken"), |
|
Arg.Do<string>(id => Guid.TryParse(id, out var _)), userId.ToString(), "testid", DeviceType.Android, |
|
Arg.Do<IEnumerable<string>>(organizationIds => |
|
{ |
|
var organizationIdsList = organizationIds.ToList(); |
|
Assert.Equal(2, organizationIdsList.Count); |
|
Assert.Contains(organizationId1.ToString(), organizationIdsList); |
|
Assert.Contains(organizationId2.ToString(), organizationIdsList); |
|
}), installationId); |
|
} |
|
|
|
/// <summary> |
|
/// Story: A user chose 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. |
|
/// </summary> |
|
[Theory, BitAutoData] |
|
public async Task UpdateDevicesTrustAsync_Works( |
|
SutProvider<DeviceService> sutProvider, |
|
Guid currentUserId, |
|
Device deviceOne, |
|
Device deviceTwo, |
|
Device deviceThree) |
|
{ |
|
SetupOldTrust(deviceOne); |
|
SetupOldTrust(deviceTwo); |
|
SetupOldTrust(deviceThree); |
|
|
|
deviceOne.Identifier = "current_device"; |
|
|
|
sutProvider.GetDependency<IDeviceRepository>() |
|
.GetManyByUserIdAsync(currentUserId) |
|
.Returns(new List<Device> { deviceOne, deviceTwo, deviceThree, }); |
|
|
|
var currentDeviceModel = new DeviceKeysUpdateRequestModel |
|
{ |
|
EncryptedPublicKey = "current_encrypted_public_key", |
|
EncryptedUserKey = "current_encrypted_user_key", |
|
}; |
|
|
|
var alteredDeviceModels = new List<OtherDeviceKeysUpdateRequestModel> |
|
{ |
|
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<IDeviceRepository>() |
|
.Received(1) |
|
.UpsertAsync(Arg.Is<Device>(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<IDeviceRepository>() |
|
.Received(1) |
|
.UpsertAsync(Arg.Is<Device>(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<IDeviceRepository>() |
|
.Received(1) |
|
.UpsertAsync(Arg.Is<Device>(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<IDeviceRepository>() |
|
.Received(3) |
|
.UpsertAsync(Arg.Any<Device>()); |
|
|
|
static void SetupOldTrust(Device device, [CallerArgumentExpression(nameof(device))] string expression = null) |
|
{ |
|
device.EncryptedPublicKey = $"old_public_{expression}"; |
|
device.EncryptedPrivateKey = $"old_private_{expression}"; |
|
device.EncryptedUserKey = $"old_user_{expression}"; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
[Theory, BitAutoData] |
|
public async Task UpdateDevicesTrustAsync_DoesNotUpdateUntrustedDevices( |
|
SutProvider<DeviceService> 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<IDeviceRepository>() |
|
.GetManyByUserIdAsync(currentUserId) |
|
.Returns(new List<Device> { deviceOne, deviceTwo, }); |
|
|
|
var currentDeviceModel = new DeviceKeysUpdateRequestModel |
|
{ |
|
EncryptedPublicKey = "current_encrypted_public_key", |
|
EncryptedUserKey = "current_encrypted_user_key", |
|
}; |
|
|
|
var alteredDeviceModels = new List<OtherDeviceKeysUpdateRequestModel> |
|
{ |
|
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<IDeviceRepository>() |
|
.Received(1) |
|
.UpsertAsync(Arg.Is<Device>(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<IDeviceRepository>() |
|
.DidNotReceive() |
|
.UpsertAsync(Arg.Is<Device>(d => d.Id == deviceTwo.Id)); |
|
} |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
[Theory, BitAutoData] |
|
public async Task UpdateDevicesTrustAsync_ThrowsNotFoundException_WhenCurrentDeviceIdentifierDoesNotExist( |
|
SutProvider<DeviceService> sutProvider, |
|
Guid currentUserId, |
|
Device deviceOne, |
|
Device deviceTwo) |
|
{ |
|
deviceOne.Identifier = "some_other_device"; |
|
deviceTwo.Identifier = "another_device"; |
|
|
|
sutProvider.GetDependency<IDeviceRepository>() |
|
.GetManyByUserIdAsync(currentUserId) |
|
.Returns(new List<Device> { deviceOne, deviceTwo, }); |
|
|
|
var currentDeviceModel = new DeviceKeysUpdateRequestModel |
|
{ |
|
EncryptedPublicKey = "current_encrypted_public_key", |
|
EncryptedUserKey = "current_encrypted_user_key", |
|
}; |
|
|
|
await Assert.ThrowsAsync<NotFoundException>(() => |
|
sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel, |
|
Enumerable.Empty<OtherDeviceKeysUpdateRequestModel>())); |
|
} |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
[Theory, BitAutoData] |
|
public async Task UpdateDevicesTrustAsync_ThrowsBadRequestException_WhenCurrentDeviceIsIncludedInAlteredDevices( |
|
SutProvider<DeviceService> sutProvider, |
|
Guid currentUserId, |
|
Device deviceOne, |
|
Device deviceTwo) |
|
{ |
|
deviceOne.Identifier = "current_device"; |
|
|
|
sutProvider.GetDependency<IDeviceRepository>() |
|
.GetManyByUserIdAsync(currentUserId) |
|
.Returns(new List<Device> { deviceOne, deviceTwo, }); |
|
|
|
var currentDeviceModel = new DeviceKeysUpdateRequestModel |
|
{ |
|
EncryptedPublicKey = "current_encrypted_public_key", |
|
EncryptedUserKey = "current_encrypted_user_key", |
|
}; |
|
|
|
var alteredDeviceModels = new List<OtherDeviceKeysUpdateRequestModel> |
|
{ |
|
new OtherDeviceKeysUpdateRequestModel |
|
{ |
|
DeviceId = deviceOne.Id, // current device is included in alteredDevices |
|
EncryptedPublicKey = "encrypted_public_key_one", |
|
EncryptedUserKey = "encrypted_user_key_one", |
|
}, |
|
}; |
|
|
|
await Assert.ThrowsAsync<BadRequestException>(() => |
|
sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel, |
|
alteredDeviceModels)); |
|
} |
|
}
|
|
|