diff --git a/src/App/App.csproj b/src/App/App.csproj
index 3c2bc8c12..4de5cea2a 100644
--- a/src/App/App.csproj
+++ b/src/App/App.csproj
@@ -266,6 +266,6 @@
-
+
diff --git a/src/App/Platforms/Android/Autofill/CredentialHelpers.cs b/src/App/Platforms/Android/Autofill/CredentialHelpers.cs
index 6153a8aa4..41c847fc9 100644
--- a/src/App/Platforms/Android/Autofill/CredentialHelpers.cs
+++ b/src/App/Platforms/Android/Autofill/CredentialHelpers.cs
@@ -1,4 +1,5 @@
-using System.Text.Json.Nodes;
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Nodes;
using Android.App;
using Android.Content;
using Android.OS;
@@ -83,23 +84,27 @@ namespace Bit.App.Platforms.Android.Autofill
if (callingRequest is null)
{
- if (ServiceContainer.TryResolve(out var deviceActionService))
- {
- await deviceActionService.DisplayAlertAsync(AppResources.ErrorCreatingPasskey, string.Empty, AppResources.Ok);
- }
+ await DisplayAlertAsync(AppResources.AnErrorHasOccurred, string.Empty);
FailAndFinish();
return;
}
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 (ServiceContainer.TryResolve(out var deviceActionService))
- {
- await deviceActionService.DisplayAlertAsync(AppResources.ErrorCreatingPasskey, AppResources.PasskeysNotSupportedForThisApp, AppResources.Ok);
- }
+ await DisplayAlertAsync(AppResources.ErrorCreatingPasskey, AppResources.PasskeysNotSupportedForThisApp);
FailAndFinish();
return;
}
@@ -202,6 +207,14 @@ namespace Bit.App.Platforms.Android.Autofill
activity.SetResult(Result.Ok, result);
activity.Finish();
+ async Task DisplayAlertAsync(string title, string message)
+ {
+ if (ServiceContainer.TryResolve(out var deviceActionService))
+ {
+ await deviceActionService.DisplayAlertAsync(title, message, AppResources.Ok);
+ }
+ }
+
void FailAndFinish()
{
var result = new Intent();
@@ -244,11 +257,11 @@ namespace Bit.App.Platforms.Android.Autofill
return extensionsJson;
}
- public static async Task LoadFido2PriviligedAllowedListAsync()
+ public static async Task LoadFido2PrivilegedAllowedListAsync()
{
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);
return reader.ReadToEnd();
@@ -266,19 +279,24 @@ namespace Bit.App.Platforms.Android.Autofill
return await ValidateAssetLinksAndGetOriginAsync(callingAppInfo, rpId);
}
- var priviligedAllowedList = await LoadFido2PriviligedAllowedListAsync();
- if (priviligedAllowedList is null)
+ var privilegedAllowedList = await LoadFido2PrivilegedAllowedListAsync();
+ 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
{
- return callingAppInfo.GetOrigin(priviligedAllowedList);
+ return callingAppInfo.GetOrigin(privilegedAllowedList);
}
catch (Java.Lang.IllegalStateException)
{
- return null; // not priviliged
+ throw new Core.Exceptions.ValidationException(AppResources.PasskeyOperationFailedBecauseBrowserSignatureDoesNotMatch);
}
catch (Java.Lang.IllegalArgumentException)
{
diff --git a/src/App/Platforms/Android/Autofill/CredentialProviderSelectionActivity.cs b/src/App/Platforms/Android/Autofill/CredentialProviderSelectionActivity.cs
index 7d406939a..5ea4213c0 100644
--- a/src/App/Platforms/Android/Autofill/CredentialProviderSelectionActivity.cs
+++ b/src/App/Platforms/Android/Autofill/CredentialProviderSelectionActivity.cs
@@ -77,7 +77,18 @@ namespace Bit.Droid.Autofill
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)
{
await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, AppResources.PasskeysNotSupportedForThisApp, AppResources.Ok);
diff --git a/src/App/Resources/Raw/fido2_priviliged_allow_list.json b/src/App/Resources/Raw/fido2_privileged_allow_list.json
similarity index 100%
rename from src/App/Resources/Raw/fido2_priviliged_allow_list.json
rename to src/App/Resources/Raw/fido2_privileged_allow_list.json
diff --git a/src/Core/Exceptions/ValidationException.cs b/src/Core/Exceptions/ValidationException.cs
new file mode 100644
index 000000000..e996148e6
--- /dev/null
+++ b/src/Core/Exceptions/ValidationException.cs
@@ -0,0 +1,10 @@
+namespace Bit.Core.Exceptions
+{
+ public class ValidationException : Exception
+ {
+ public ValidationException(string localizedMessage)
+ : base(localizedMessage)
+ {
+ }
+ }
+}
diff --git a/src/Core/Resources/Localization/AppResources.Designer.cs b/src/Core/Resources/Localization/AppResources.Designer.cs
index cbe3adf4e..d3d1003a5 100644
--- a/src/Core/Resources/Localization/AppResources.Designer.cs
+++ b/src/Core/Resources/Localization/AppResources.Designer.cs
@@ -5263,6 +5263,51 @@ namespace Bit.Core.Resources.Localization {
}
}
+ ///
+ /// Looks up a localized string similar to Passkey operation failed because app could not be verified.
+ ///
+ public static string PasskeyOperationFailedBecauseAppCouldNotBeVerified {
+ get {
+ return ResourceManager.GetString("PasskeyOperationFailedBecauseAppCouldNotBeVerified", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Passkey operation failed because app not found in asset links.
+ ///
+ public static string PasskeyOperationFailedBecauseAppNotFoundInAssetLinks {
+ get {
+ return ResourceManager.GetString("PasskeyOperationFailedBecauseAppNotFoundInAssetLinks", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Passkey operation failed because browser is not privileged.
+ ///
+ public static string PasskeyOperationFailedBecauseBrowserIsNotPrivileged {
+ get {
+ return ResourceManager.GetString("PasskeyOperationFailedBecauseBrowserIsNotPrivileged", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Passkey operation failed because browser signature does not match.
+ ///
+ public static string PasskeyOperationFailedBecauseBrowserSignatureDoesNotMatch {
+ get {
+ return ResourceManager.GetString("PasskeyOperationFailedBecauseBrowserSignatureDoesNotMatch", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Passkey operation failed because of missing asset links.
+ ///
+ public static string PasskeyOperationFailedBecauseOfMissingAssetLinks {
+ get {
+ return ResourceManager.GetString("PasskeyOperationFailedBecauseOfMissingAssetLinks", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Passkeys.
///
diff --git a/src/Core/Resources/Localization/AppResources.resx b/src/Core/Resources/Localization/AppResources.resx
index d0940a16d..b3d0bfb71 100644
--- a/src/Core/Resources/Localization/AppResources.resx
+++ b/src/Core/Resources/Localization/AppResources.resx
@@ -2990,4 +2990,19 @@ Do you want to switch to this account?
Passkeys not supported for this app
+
+ Passkey operation failed because browser is not privileged
+
+
+ Passkey operation failed because browser signature does not match
+
+
+ Passkey operation failed because of missing asset links
+
+
+ Passkey operation failed because app not found in asset links
+
+
+ Passkey operation failed because app could not be verified
+
diff --git a/src/Core/Services/AssetLinksService.cs b/src/Core/Services/AssetLinksService.cs
index 87c61bb7a..1393dc204 100644
--- a/src/Core/Services/AssetLinksService.cs
+++ b/src/Core/Services/AssetLinksService.cs
@@ -1,4 +1,5 @@
using Bit.Core.Abstractions;
+using Bit.Core.Resources.Localization;
namespace Bit.Core.Services
{
@@ -18,18 +19,35 @@ namespace Bit.Core.Services
/// True if matches, False otherwise.
public async Task ValidateAssetLinksAsync(string rpId, string packageName, string normalizedFingerprint)
{
- var statementList = await _apiService.GetDigitalAssetLinksForRpAsync(rpId);
+ try
+ {
+ var statementList = await _apiService.GetDigitalAssetLinksForRpAsync(rpId);
- return statementList
- .Any(s => s.Target.Namespace == "android_app"
- &&
- s.Target.PackageName == packageName
- &&
- s.Relation.Contains("delegate_permission/common.get_login_creds")
- &&
- s.Relation.Contains("delegate_permission/common.handle_all_urls")
- &&
- s.Target.Sha256CertFingerprints.Contains(normalizedFingerprint));
+ var androidAppPackageStatements = statementList
+ .Where(s => s.Target.Namespace == "android_app"
+ &&
+ s.Target.PackageName == packageName
+ &&
+ s.Relation.Contains("delegate_permission/common.get_login_creds")
+ &&
+ s.Relation.Contains("delegate_permission/common.handle_all_urls"));
+
+ 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);
+ }
}
}
}
diff --git a/test/Core.Test/Services/AssetLinksServiceTest.cs b/test/Core.Test/Services/AssetLinksServiceTest.cs
index f2bd79d58..fee01cbb3 100644
--- a/test/Core.Test/Services/AssetLinksServiceTest.cs
+++ b/test/Core.Test/Services/AssetLinksServiceTest.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
+using Bit.Core.Resources.Localization;
using Bit.Core.Services;
using Bit.Core.Utilities.DigitalAssetLinks;
using Bit.Test.Common.AutoFixture;
@@ -71,7 +72,7 @@ namespace Bit.Core.Test.Services
}
[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
_sutProvider.GetDependency()
@@ -79,14 +80,14 @@ namespace Bit.Core.Test.Services
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementNoGetLoginCredsRelationJson())));
// Act
- var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint);
+ var exception = await Assert.ThrowsAsync(() => _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint));
// Assert
- Assert.False(isValid);
+ Assert.Equal(AppResources.PasskeyOperationFailedBecauseAppNotFoundInAssetLinks, exception.Message);
}
[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
_sutProvider.GetDependency()
@@ -94,14 +95,14 @@ namespace Bit.Core.Test.Services
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementNoHandleAllUrlsRelationJson())));
// Act
- var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint);
+ var exception = await Assert.ThrowsAsync(() => _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint));
// Assert
- Assert.False(isValid);
+ Assert.Equal(AppResources.PasskeyOperationFailedBecauseAppNotFoundInAssetLinks, exception.Message);
}
[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
_sutProvider.GetDependency()
@@ -109,14 +110,14 @@ namespace Bit.Core.Test.Services
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementWrongNamespaceJson())));
// Act
- var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint);
+ var exception = await Assert.ThrowsAsync(() => _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint));
// Assert
- Assert.False(isValid);
+ Assert.Equal(AppResources.PasskeyOperationFailedBecauseAppNotFoundInAssetLinks, exception.Message);
}
[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
_sutProvider.GetDependency()
@@ -124,14 +125,30 @@ namespace Bit.Core.Test.Services
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementNoFingerprintsJson())));
// Act
- var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint);
+ var exception = await Assert.ThrowsAsync(() => _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint));
// Assert
- Assert.False(isValid);
+ Assert.Equal(AppResources.PasskeyOperationFailedBecauseAppCouldNotBeVerified, exception.Message);
}
[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()
+ .GetDigitalAssetLinksForRpAsync(_validRpId)
+ .Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementOneFingerprintJson())));
+
+
+ // Act
+ var exception = await Assert.ThrowsAsync(() => _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
_sutProvider.GetDependency()
@@ -139,25 +156,10 @@ namespace Bit.Core.Test.Services
.Returns(Task.FromResult(Deserialize(BasicAssetLinksTestData.OneStatementOneFingerprintJson())));
// Act
- var isValid = await _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, "com.foo.another", _validFingerprint);
+ var exception = await Assert.ThrowsAsync(() => _sutProvider.Sut.ValidateAssetLinksAsync(_validRpId, _validPackageName, _validFingerprint.Replace("00", "33")));
// Assert
- Assert.False(isValid);
- }
-
- [Fact]
- public async Task ValidateAssetLinksAsync_Returns_False_When_Data_Fingerprint_Doesnt_Match()
- {
- // Arrange
- _sutProvider.GetDependency()
- .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);
+ Assert.Equal(AppResources.PasskeyOperationFailedBecauseAppCouldNotBeVerified, exception.Message);
}
public void Dispose() {}