Skip to content
Merged
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
180 changes: 156 additions & 24 deletions src/Kp2aBusinessLogic/Io/OneDrive2FileStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using System.Reflection;
using System.Text;
using Android.Content;
using Android.Preferences;
using Android.Util;
using KeePass.Util;
using keepass2android.Io.ItemLocation;
Expand Down Expand Up @@ -198,6 +199,43 @@ public OneDrive2ItemLocation<OneDrive2PrefixContainerType> BuildShare(string id,
public static IPublicClientApplication _publicClientApp = null;
private string ClientID = "8374f801-0f55-407d-80cc-9a04fe86d9b2";

// MSAL broker (WAM/Authenticator/Company Portal) is required to pass the device ID for
// Intune/Entra-managed devices, but broken broker setups (missing package visibility,
// missing broker redirect URI registration, ...) must not block ordinary OneDrive sign-in.
// The user-facing "OneDrive device sign-in" preference (pref_app_file_handling.xml) picks
// between: Auto (try broker, permanently fall back to non-broker for this device on a
// broker-specific failure), Always (force broker, surface real errors, no fallback -
// for admins who need to confirm/require the Intune device-ID path), and Never (skip
// broker entirely - the escape hatch for devices where broker itself is broken).
private enum BrokerMode
{
Auto,
Always,
Never
}

// Must match Resource.String.OneDriveBrokerMode_key's value (config.xml) - this business-logic
// assembly has no access to the app's generated Resource class, so the key is duplicated here.
private const string PrefKeyBrokerMode = "OneDriveBrokerMode";
private const string BrokerModeValueAlways = "ALWAYS";
private const string BrokerModeValueNever = "NEVER";

// Sticky per-device flag learned in Auto mode once broker is seen to fail; independent of
// PrefKeyBrokerMode so switching back to Auto later remembers the outcome.
private const string PrefKeyBrokerAvailable = "OneDrive2FileStorageBrokerAvailable";

private static readonly HashSet<string> BrokerFailureErrorCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"cannot_invoke_broker",
"no_broker_installed_on_device",
"no_broker_account_found",
"null_intent_returned_from_broker",
"broker_signature_verification_failed",
"android_broker_operation_failed",
};

private static BrokerMode _brokerMode;
private static bool _brokerEnabledForCurrentClient;

public abstract IEnumerable<string> Scopes
{
Expand All @@ -206,10 +244,83 @@ public abstract IEnumerable<string> Scopes

public OneDrive2FileStorage()
{
_publicClientApp = PublicClientApplicationBuilder.Create(ClientID)
.WithRedirectUri($"msal{ClientID}://auth")
.WithBroker(true) // Enable broker authentication to pass device ID for Intune/Entra managed devices
.Build();
_brokerMode = ReadBrokerModePreference();
_brokerEnabledForCurrentClient = _brokerMode switch
{
BrokerMode.Always => true,
BrokerMode.Never => false,
_ => IsBrokerAllowedByCache(),
};
_publicClientApp = BuildPublicClientApp(_brokerEnabledForCurrentClient);
}

private IPublicClientApplication BuildPublicClientApp(bool useBroker)
{
var builder = PublicClientApplicationBuilder.Create(ClientID)
.WithRedirectUri($"msal{ClientID}://auth");
if (useBroker)
builder = builder.WithBroker(true);
return builder.Build();
}

private BrokerMode ReadBrokerModePreference()
{
try
{
string value = PreferenceManager.GetDefaultSharedPreferences(Android.App.Application.Context)
.GetString(PrefKeyBrokerMode, null);
if (value == BrokerModeValueAlways)
return BrokerMode.Always;
if (value == BrokerModeValueNever)
return BrokerMode.Never;
return BrokerMode.Auto;
}
catch (Exception e)
{
logDebug("failed to read broker-mode preference: " + e);
return BrokerMode.Auto;
}
}

private bool IsBrokerAllowedByCache()
{
try
{
return PreferenceManager.GetDefaultSharedPreferences(Android.App.Application.Context)
.GetBoolean(PrefKeyBrokerAvailable, true);
}
catch (Exception e)
{
logDebug("failed to read broker-available preference: " + e);
return true;
}
}

private static bool IsBrokerFailure(MsalException ex)
{
return ex.ErrorCode != null && BrokerFailureErrorCodes.Contains(ex.ErrorCode);
}

// Only Auto mode auto-recovers from a broker failure by falling back to non-broker; Always
// mode intentionally surfaces the real error instead of silently masking a broken broker.
private static bool ShouldFallBackFromBroker(MsalException ex)
{
return _brokerMode == BrokerMode.Auto && _brokerEnabledForCurrentClient && IsBrokerFailure(ex);
}

private void DisableBrokerAndRebuildClient()
{
_brokerEnabledForCurrentClient = false;
_publicClientApp = BuildPublicClientApp(false);
try
{
PreferenceManager.GetDefaultSharedPreferences(Android.App.Application.Context)
.Edit().PutBoolean(PrefKeyBrokerAvailable, false).Commit();
}
catch (Exception e)
{
logDebug("failed to persist broker-unavailable preference: " + e);
}
}

class PathItemBuilder
Expand Down Expand Up @@ -993,36 +1104,48 @@ protected void FinishActivityWithSuccess(

public async void OnStart(IFileStorageSetupActivity activity)
{
logDebug("OneDrive2.OnStart");
if (activity.ProcessName.Equals(FileStorageSetupDefs.ProcessNameFileUsageSetup))
activity.State.PutString(FileStorageSetupDefs.ExtraPath, activity.Ioc.Path);
string rootPathForUser = await TryLoginSilent(activity.Ioc.Path);
if (rootPathForUser != null)
{
logDebug("rootPathForUser not null");
FinishActivityWithSuccess(activity, rootPathForUser);
return;
}
logDebug("rootPathForUser null");

try
{
logDebug("OneDrive2.OnStart");
if (activity.ProcessName.Equals(FileStorageSetupDefs.ProcessNameFileUsageSetup))
activity.State.PutString(FileStorageSetupDefs.ExtraPath, activity.Ioc.Path);
string rootPathForUser = await TryLoginSilent(activity.Ioc.Path);
if (rootPathForUser != null)
{
logDebug("rootPathForUser not null");
FinishActivityWithSuccess(activity, rootPathForUser);
return;
}
logDebug("rootPathForUser null");

logDebug("try interactive");
AuthenticationResult res = await _publicClientApp.AcquireTokenInteractive(Scopes)
.WithParentActivityOrWindow((Activity)activity)
.ExecuteAsync();
AuthenticationResult res;
try
{
res = await _publicClientApp.AcquireTokenInteractive(Scopes)
.WithParentActivityOrWindow((Activity)activity)
.ExecuteAsync();
}
catch (MsalException msalEx) when (ShouldFallBackFromBroker(msalEx))
{
logDebug("interactive auth failed with broker error " + msalEx.ErrorCode + ", retrying without broker");
DisableBrokerAndRebuildClient();
res = await _publicClientApp.AcquireTokenInteractive(Scopes)
.WithParentActivityOrWindow((Activity)activity)
.ExecuteAsync();
}
logDebug("ok interactive");
BuildClient(res);
FinishActivityWithSuccess(activity, BuildRootPathForUser(res));


}
catch (Exception e)
{
logDebug("authenticating not successful: " + e);
string errorMessage = "authenticating not successful";
if (e is MsalException msalEx)
errorMessage += " (" + msalEx.ErrorCode + ")";
logDebug(errorMessage + ": " + e);
Intent data = new Intent();
data.PutExtra(FileStorageSetupDefs.ExtraErrorMessage, "authenticating not successful");
data.PutExtra(FileStorageSetupDefs.ExtraErrorMessage, errorMessage);
((Activity)activity).SetResult(Result.Canceled, data);
((Activity)activity).Finish();
}
Expand Down Expand Up @@ -1084,9 +1207,18 @@ private async Task<string> TryLoginSilent(string iocPath)
logDebug("ui required");
return null;
}
catch (MsalException msalEx) when (ShouldFallBackFromBroker(msalEx))
{
// The broker-backed account/token cache is not usable once broker itself is broken.
// Disable broker for future logins and fall back to a fresh interactive sign-in.
logDebug("silent auth failed with broker error " + msalEx.ErrorCode + ", disabling broker for future logins");
DisableBrokerAndRebuildClient();
return null;
}
catch (Exception ex)
{
logDebug("silent login failed: " + ex.ToString());
string? errorCode = (ex as MsalException)?.ErrorCode;
logDebug("silent login failed" + (errorCode != null ? " (" + errorCode + ")" : "") + ": " + ex);
return null;
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/keepass2android-app/Manifests/AndroidManifest_debug.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
<package android:name="com.inputstick.apps.kp2aplugin" />
<package android:name="com.dropbox.android" />

<!-- MSAL broker apps for OneDrive Intune/Entra-managed device authentication -->
<package android:name="com.azure.authenticator" />
<package android:name="com.microsoft.windowsintune.companyportal" />
<package android:name="com.microsoft.workaccount" />

<intent>
<action android:name="android.intent.action.OPEN_DOCUMENT" />
<data android:mimeType="*/*" />
Expand Down
5 changes: 5 additions & 0 deletions src/keepass2android-app/Manifests/AndroidManifest_net.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
<package android:name="com.inputstick.apps.kp2aplugin" />
<package android:name="com.dropbox.android" />

<!-- MSAL broker apps for OneDrive Intune/Entra-managed device authentication -->
<package android:name="com.azure.authenticator" />
<package android:name="com.microsoft.windowsintune.companyportal" />
<package android:name="com.microsoft.workaccount" />

<intent>
<action android:name="android.intent.action.OPEN_DOCUMENT" />
<data android:mimeType="*/*" />
Expand Down
8 changes: 8 additions & 0 deletions src/keepass2android-app/Resources/values/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,14 @@
<item>ERROR</item>
</string-array>

<string name="OneDriveBrokerMode_key">OneDriveBrokerMode</string>
<string name="OneDriveBrokerMode_default">AUTO</string>
<string-array name="OneDriveBrokerMode_values">
<item>AUTO</item>
<item>ALWAYS</item>
<item>NEVER</item>
</string-array>

<string name="ShowUnlockedNotification_key">ShowUnlockedNotification</string>
<bool name="ShowUnlockedNotification_default">true</bool>
<integer name="WebDavChunkedUploadSize_default">65536</integer>
Expand Down
7 changes: 7 additions & 0 deletions src/keepass2android-app/Resources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,8 @@
<string name="show_local_backups">Show local backups</string>
<string name="AcceptAllServerCertificates_title">SSL certificates</string>
<string name="AcceptAllServerCertificates_summary">Define the behavior when certificate validation fails. Note: you can install certificates on your device if validation fails!</string>
<string name="OneDriveBrokerMode_title">OneDrive device sign-in (broker)</string>
<string name="OneDriveBrokerMode_summary">Controls whether OneDrive sign-in uses your device\'s Microsoft account manager (broker), which is required on some Intune/Entra-managed work devices. Automatic tries broker sign-in first and falls back to normal sign-in if it fails. Choose "Never" if OneDrive sign-in fails repeatedly on this device.</string>
<string name="ClearOfflineCache_title">Clear cache?</string>
<string name="ClearOfflineCache_question">This will delete all cached database files. Any changes you made while having no access to the source database and which have not yet been synchronized will be lost! Continue?</string>
<string name="CheckForFileChangesOnSave_title">Check for modifications</string>
Expand Down Expand Up @@ -1282,6 +1284,11 @@
<item>Warn when validation fails</item>
<item>Do not accept invalid certificates</item>
</string-array>
<string-array name="OneDriveBrokerMode_options">
<item>Automatic (recommended)</item>
<item>Always use device sign-in</item>
<item>Never use device sign-in</item>
</string-array>
<string name="ClearClipboardWarning">Make sure this works on your system and consider using the built-in keyboard if not.</string>
<string name="PluginDescription">Description provided by the plugin:</string>
<string name="autofill_hint">Keepass2Android supports Android\'s Autofill feature but it looks like you haven\'t enabled it yet.</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@
android:dialogTitle="@string/AcceptAllServerCertificates_title"
android:defaultValue="@string/AcceptAllServerCertificates_default" />

<ListPreference
android:key="@string/OneDriveBrokerMode_key"
android:title="@string/OneDriveBrokerMode_title"
android:summary="@string/OneDriveBrokerMode_summary"
android:entries="@array/OneDriveBrokerMode_options"
android:entryValues="@array/OneDriveBrokerMode_values"
android:dialogTitle="@string/OneDriveBrokerMode_title"
android:defaultValue="@string/OneDriveBrokerMode_default" />


<CheckBoxPreference
android:enabled="true"
Expand Down
Loading