PM-7746 Added specific validation messages for (non) privileged apps validation on Fido2 flows. Also fixed typo on "privileged" and updated UT (#3198)

This commit is contained in:
Federico Maccaroni 2024-04-26 13:59:03 -03:00 committed by GitHub
parent ba1183234b
commit f80ec1b221
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 179 additions and 60 deletions

View file

@ -266,6 +266,6 @@
<BundleResource Include="Platforms\iOS\PrivacyInfo.xcprivacy" LogicalName="PrivacyInfo.xcprivacy" /> <BundleResource Include="Platforms\iOS\PrivacyInfo.xcprivacy" LogicalName="PrivacyInfo.xcprivacy" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'"> <ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
<MauiAsset Include="Resources\Raw\fido2_priviliged_allow_list.json" LogicalName="fido2_priviliged_allow_list.json" /> <MauiAsset Include="Resources\Raw\fido2_privileged_allow_list.json" LogicalName="fido2_privileged_allow_list.json" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,4 +1,5 @@
using System.Text.Json.Nodes; using System.ComponentModel.DataAnnotations;
using System.Text.Json.Nodes;
using Android.App; using Android.App;
using Android.Content; using Android.Content;
using Android.OS; using Android.OS;
@ -83,23 +84,27 @@ namespace Bit.App.Platforms.Android.Autofill
if (callingRequest is null) if (callingRequest is null)
{ {
if (ServiceContainer.TryResolve<IDeviceActionService>(out var deviceActionService)) await DisplayAlertAsync(AppResources.AnErrorHasOccurred, string.Empty);
{
await deviceActionService.DisplayAlertAsync(AppResources.ErrorCreatingPasskey, string.Empty, AppResources.Ok);
}
FailAndFinish(); FailAndFinish();
return; return;
} }
var credentialCreationOptions = GetPublicKeyCredentialCreationOptionsFromJson(callingRequest.RequestJson); var credentialCreationOptions = GetPublicKeyCredentialCreationOptionsFromJson(callingRequest.RequestJson);
var origin = await ValidateCallingAppInfoAndGetOriginAsync(getRequest.CallingAppInfo, credentialCreationOptions.Rp.Id); string origin;
try
{
origin = await ValidateCallingAppInfoAndGetOriginAsync(getRequest.CallingAppInfo, credentialCreationOptions.Rp.Id);
}
catch (Core.Exceptions.ValidationException valEx)
{
await DisplayAlertAsync(AppResources.AnErrorHasOccurred, valEx.Message);
FailAndFinish();
return;
}
if (origin is null) if (origin is null)
{ {
if (ServiceContainer.TryResolve<IDeviceActionService>(out var deviceActionService)) await DisplayAlertAsync(AppResources.ErrorCreatingPasskey, AppResources.PasskeysNotSupportedForThisApp);
{
await deviceActionService.DisplayAlertAsync(AppResources.ErrorCreatingPasskey, AppResources.PasskeysNotSupportedForThisApp, AppResources.Ok);
}
FailAndFinish(); FailAndFinish();
return; return;
} }
@ -202,6 +207,14 @@ namespace Bit.App.Platforms.Android.Autofill
activity.SetResult(Result.Ok, result); activity.SetResult(Result.Ok, result);
activity.Finish(); activity.Finish();
async Task DisplayAlertAsync(string title, string message)
{
if (ServiceContainer.TryResolve<IDeviceActionService>(out var deviceActionService))
{
await deviceActionService.DisplayAlertAsync(title, message, AppResources.Ok);
}
}
void FailAndFinish() void FailAndFinish()
{ {
var result = new Intent(); var result = new Intent();
@ -244,11 +257,11 @@ namespace Bit.App.Platforms.Android.Autofill
return extensionsJson; return extensionsJson;
} }
public static async Task<string> LoadFido2PriviligedAllowedListAsync() public static async Task<string> LoadFido2PrivilegedAllowedListAsync()
{ {
try try
{ {
using var stream = await FileSystem.OpenAppPackageFileAsync("fido2_priviliged_allow_list.json"); using var stream = await FileSystem.OpenAppPackageFileAsync("fido2_privileged_allow_list.json");
using var reader = new StreamReader(stream); using var reader = new StreamReader(stream);
return reader.ReadToEnd(); return reader.ReadToEnd();
@ -266,19 +279,24 @@ namespace Bit.App.Platforms.Android.Autofill
return await ValidateAssetLinksAndGetOriginAsync(callingAppInfo, rpId); return await ValidateAssetLinksAndGetOriginAsync(callingAppInfo, rpId);
} }
var priviligedAllowedList = await LoadFido2PriviligedAllowedListAsync(); var privilegedAllowedList = await LoadFido2PrivilegedAllowedListAsync();
if (priviligedAllowedList is null) if (privilegedAllowedList is null)
{ {
throw new InvalidOperationException("Could not load Fido2 priviliged allowed list"); throw new InvalidOperationException("Could not load Fido2 privileged allowed list");
}
if (!privilegedAllowedList.Contains($"\"package_name\": \"{callingAppInfo.PackageName}\""))
{
throw new Core.Exceptions.ValidationException(AppResources.PasskeyOperationFailedBecauseBrowserIsNotPrivileged);
} }
try try
{ {
return callingAppInfo.GetOrigin(priviligedAllowedList); return callingAppInfo.GetOrigin(privilegedAllowedList);
} }
catch (Java.Lang.IllegalStateException) catch (Java.Lang.IllegalStateException)
{ {
return null; // not priviliged throw new Core.Exceptions.ValidationException(AppResources.PasskeyOperationFailedBecauseBrowserSignatureDoesNotMatch);
} }
catch (Java.Lang.IllegalArgumentException) catch (Java.Lang.IllegalArgumentException)
{ {

View file

@ -77,7 +77,18 @@ namespace Bit.Droid.Autofill
var packageName = getRequest.CallingAppInfo.PackageName; var packageName = getRequest.CallingAppInfo.PackageName;
var origin = await CredentialHelpers.ValidateCallingAppInfoAndGetOriginAsync(getRequest.CallingAppInfo, RpId); string origin;
try
{
origin = await CredentialHelpers.ValidateCallingAppInfoAndGetOriginAsync(getRequest.CallingAppInfo, RpId);
}
catch (Core.Exceptions.ValidationException valEx)
{
await _deviceActionService.Value.DisplayAlertAsync(AppResources.AnErrorHasOccurred, valEx.Message, AppResources.Ok);
FailAndFinish();
return;
}
if (origin is null) if (origin is null)
{ {
await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, AppResources.PasskeysNotSupportedForThisApp, AppResources.Ok); await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, AppResources.PasskeysNotSupportedForThisApp, AppResources.Ok);

View file

@ -0,0 +1,10 @@
namespace Bit.Core.Exceptions
{
public class ValidationException : Exception
{
public ValidationException(string localizedMessage)
: base(localizedMessage)
{
}
}
}

View file

@ -5263,6 +5263,51 @@ namespace Bit.Core.Resources.Localization {
} }
} }
/// <summary>
/// Looks up a localized string similar to Passkey operation failed because app could not be verified.
/// </summary>
public static string PasskeyOperationFailedBecauseAppCouldNotBeVerified {
get {
return ResourceManager.GetString("PasskeyOperationFailedBecauseAppCouldNotBeVerified", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Passkey operation failed because app not found in asset links.
/// </summary>
public static string PasskeyOperationFailedBecauseAppNotFoundInAssetLinks {
get {
return ResourceManager.GetString("PasskeyOperationFailedBecauseAppNotFoundInAssetLinks", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Passkey operation failed because browser is not privileged.
/// </summary>
public static string PasskeyOperationFailedBecauseBrowserIsNotPrivileged {
get {
return ResourceManager.GetString("PasskeyOperationFailedBecauseBrowserIsNotPrivileged", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Passkey operation failed because browser signature does not match.
/// </summary>
public static string PasskeyOperationFailedBecauseBrowserSignatureDoesNotMatch {
get {
return ResourceManager.GetString("PasskeyOperationFailedBecauseBrowserSignatureDoesNotMatch", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Passkey operation failed because of missing asset links.
/// </summary>
public static string PasskeyOperationFailedBecauseOfMissingAssetLinks {
get {
return ResourceManager.GetString("PasskeyOperationFailedBecauseOfMissingAssetLinks", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Passkeys. /// Looks up a localized string similar to Passkeys.
/// </summary> /// </summary>

View file

@ -2990,4 +2990,19 @@ Do you want to switch to this account?</value>
<data name="PasskeysNotSupportedForThisApp" xml:space="preserve"> <data name="PasskeysNotSupportedForThisApp" xml:space="preserve">
<value>Passkeys not supported for this app</value> <value>Passkeys not supported for this app</value>
</data> </data>
<data name="PasskeyOperationFailedBecauseBrowserIsNotPrivileged" xml:space="preserve">
<value>Passkey operation failed because browser is not privileged</value>
</data>
<data name="PasskeyOperationFailedBecauseBrowserSignatureDoesNotMatch" xml:space="preserve">
<value>Passkey operation failed because browser signature does not match</value>
</data>
<data name="PasskeyOperationFailedBecauseOfMissingAssetLinks" xml:space="preserve">
<value>Passkey operation failed because of missing asset links</value>
</data>
<data name="PasskeyOperationFailedBecauseAppNotFoundInAssetLinks" xml:space="preserve">
<value>Passkey operation failed because app not found in asset links</value>
</data>
<data name="PasskeyOperationFailedBecauseAppCouldNotBeVerified" xml:space="preserve">
<value>Passkey operation failed because app could not be verified</value>
</data>
</root> </root>

View file

@ -1,4 +1,5 @@
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Resources.Localization;
namespace Bit.Core.Services namespace Bit.Core.Services
{ {
@ -18,18 +19,35 @@ namespace Bit.Core.Services
/// <returns><c>True</c> if matches, <c>False</c> otherwise.</returns> /// <returns><c>True</c> if matches, <c>False</c> otherwise.</returns>
public async Task<bool> ValidateAssetLinksAsync(string rpId, string packageName, string normalizedFingerprint) public async Task<bool> ValidateAssetLinksAsync(string rpId, string packageName, string normalizedFingerprint)
{ {
var statementList = await _apiService.GetDigitalAssetLinksForRpAsync(rpId); try
{
var statementList = await _apiService.GetDigitalAssetLinksForRpAsync(rpId);
return statementList var androidAppPackageStatements = statementList
.Any(s => s.Target.Namespace == "android_app" .Where(s => s.Target.Namespace == "android_app"
&& &&
s.Target.PackageName == packageName s.Target.PackageName == packageName
&& &&
s.Relation.Contains("delegate_permission/common.get_login_creds") s.Relation.Contains("delegate_permission/common.get_login_creds")
&& &&
s.Relation.Contains("delegate_permission/common.handle_all_urls") s.Relation.Contains("delegate_permission/common.handle_all_urls"));
&&
s.Target.Sha256CertFingerprints.Contains(normalizedFingerprint)); if (!androidAppPackageStatements.Any())
{
throw new Exceptions.ValidationException(AppResources.PasskeyOperationFailedBecauseAppNotFoundInAssetLinks);
}
if (!androidAppPackageStatements.Any(s => s.Target.Sha256CertFingerprints.Contains(normalizedFingerprint)))
{
throw new Exceptions.ValidationException(AppResources.PasskeyOperationFailedBecauseAppCouldNotBeVerified);
}
return true;
}
catch (Exceptions.ApiException)
{
throw new Exceptions.ValidationException(AppResources.PasskeyOperationFailedBecauseOfMissingAssetLinks);
}
} }
} }
} }

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Resources.Localization;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities.DigitalAssetLinks; using Bit.Core.Utilities.DigitalAssetLinks;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
@ -71,7 +72,7 @@ namespace Bit.Core.Test.Services
} }
[Fact] [Fact]
public async Task ValidateAssetLinksAsync_Returns_False_When_Data_Statement_Has_No_GetLoginCreds_Relation() public async Task ValidateAssetLinksAsync_Throws_When_Data_Statement_Has_No_GetLoginCreds_Relation()
{ {
// Arrange // Arrange
_sutProvider.GetDependency<IApiService>() _sutProvider.GetDependency<IApiService>()
@ -79,14 +80,14 @@ namespace Bit.Core.Test.Services
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementNoGetLoginCredsRelationJson()))); .Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementNoGetLoginCredsRelationJson())));
// Act // Act
var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint); var exception = await Assert.ThrowsAsync<Exceptions.ValidationException>(() => _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint));
// Assert // Assert
Assert.False(isValid); Assert.Equal(AppResources.PasskeyOperationFailedBecauseAppNotFoundInAssetLinks, exception.Message);
} }
[Fact] [Fact]
public async Task ValidateAssetLinksAsync_Returns_False_When_Data_Statement_Has_No_HandleAllUrls_Relation() public async Task ValidateAssetLinksAsync_Throws_When_Data_Statement_Has_No_HandleAllUrls_Relation()
{ {
// Arrange // Arrange
_sutProvider.GetDependency<IApiService>() _sutProvider.GetDependency<IApiService>()
@ -94,14 +95,14 @@ namespace Bit.Core.Test.Services
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementNoHandleAllUrlsRelationJson()))); .Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementNoHandleAllUrlsRelationJson())));
// Act // Act
var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint); var exception = await Assert.ThrowsAsync<Exceptions.ValidationException>(() => _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint));
// Assert // Assert
Assert.False(isValid); Assert.Equal(AppResources.PasskeyOperationFailedBecauseAppNotFoundInAssetLinks, exception.Message);
} }
[Fact] [Fact]
public async Task ValidateAssetLinksAsync_Returns_False_When_Data_Statement_Has_Wrong_Namespace() public async Task ValidateAssetLinksAsync_Throws_When_Data_Statement_Has_Wrong_Namespace()
{ {
// Arrange // Arrange
_sutProvider.GetDependency<IApiService>() _sutProvider.GetDependency<IApiService>()
@ -109,14 +110,14 @@ namespace Bit.Core.Test.Services
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementWrongNamespaceJson()))); .Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementWrongNamespaceJson())));
// Act // Act
var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint); var exception = await Assert.ThrowsAsync<Exceptions.ValidationException>(() => _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint));
// Assert // Assert
Assert.False(isValid); Assert.Equal(AppResources.PasskeyOperationFailedBecauseAppNotFoundInAssetLinks, exception.Message);
} }
[Fact] [Fact]
public async Task ValidateAssetLinksAsync_Returns_False_When_Data_Statement_Has_No_Fingerprints() public async Task ValidateAssetLinksAsync_Throws_When_Data_Statement_Has_No_Fingerprints()
{ {
// Arrange // Arrange
_sutProvider.GetDependency<IApiService>() _sutProvider.GetDependency<IApiService>()
@ -124,14 +125,30 @@ namespace Bit.Core.Test.Services
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementNoFingerprintsJson()))); .Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementNoFingerprintsJson())));
// Act // Act
var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint); var exception = await Assert.ThrowsAsync<Exceptions.ValidationException>(() => _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint));
// Assert // Assert
Assert.False(isValid); Assert.Equal(AppResources.PasskeyOperationFailedBecauseAppCouldNotBeVerified, exception.Message);
} }
[Fact] [Fact]
public async Task ValidateAssetLinksAsync_Returns_False_When_Data_PackageName_Doesnt_Match() public async Task ValidateAssetLinksAsync_Throws_When_Data_PackageName_Doesnt_Match()
{
// Arrange
_sutProvider.GetDependency<IApiService>()
.GetDigitalAssetLinksForRpAsync(_validRpId)
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementOneFingerprintJson())));
// Act
var exception = await Assert.ThrowsAsync<Exceptions.ValidationException>(() => _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, "com.foo.another", _validFingerprint));
// Assert
Assert.Equal(AppResources.PasskeyOperationFailedBecauseAppNotFoundInAssetLinks, exception.Message);
}
[Fact]
public async Task ValidateAssetLinksAsync_Throws_When_Data_Fingerprint_Doesnt_Match()
{ {
// Arrange // Arrange
_sutProvider.GetDependency<IApiService>() _sutProvider.GetDependency<IApiService>()
@ -139,25 +156,10 @@ namespace Bit.Core.Test.Services
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementOneFingerprintJson()))); .Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementOneFingerprintJson())));
// Act // Act
var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, "com.foo.another", _validFingerprint); var exception = await Assert.ThrowsAsync<Exceptions.ValidationException>(() => _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint.Replace("00", "33")));
// Assert // Assert
Assert.False(isValid); Assert.Equal(AppResources.PasskeyOperationFailedBecauseAppCouldNotBeVerified, exception.Message);
}
[Fact]
public async Task ValidateAssetLinksAsync_Returns_False_When_Data_Fingerprint_Doesnt_Match()
{
// Arrange
_sutProvider.GetDependency<IApiService>()
.GetDigitalAssetLinksForRpAsync(_validRpId)
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementOneFingerprintJson())));
// Act
var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint.Replace("00", "33"));
// Assert
Assert.False(isValid);
} }
public void Dispose() {} public void Dispose() {}