Skip to content

KeePassCore (WinForms app) roadmap

src/KeePassCore, net10.0-windows, WinForms. Will be renamed KeePassCore.WinApp.

Goal and rules

  • Only a demo proving the lib works. Windows only (tested on Wine so far). No Unix branches: Unix support belongs to the lib or the CLI.
  • Receives the Windows-specific code moved out of the lib.
  • P/Invoke: [LibraryImport] only, explicit ...W entry points with StringMarshalling.Utf16, no StringBuilder (char[] + NativeMethods.StringFromBuffer), blittable structs (otherwise wrap with Marshal.StructureToPtr like TaskDialogIndirect).
  • Images: SkiaSharp. System.Drawing and then I would force window-only code again. With SkiaSharp I let the way to develop a front with Avalonia, MAUI, ...

Status

Area Status
Unix / Mono code and leftovers Done
DllImport → [LibraryImport] Done (not run on Windows yet)
Image handling (GfxUtil) taken over from the lib Done
NativeLib → ProcessUtil, DpapiString, IUIOperations Done
Tests project (tests/KeePassCore.Tests) Done
KeePassLibC calls removed Done (process protection: call to enable on Windows)
KDB (KeePass 1.x) and KDBX 3.1 import / export Done (removed)
.NET Framework AppContext switches (InitAppContext) Done
Other code for old .NET Framework versions and Windows versions Done (PLGX kept until the plugin redesign)
Run on real Windows To do
Rename to KeePassCore.WinApp To do (tooling ready)
Configuration in JSON Done
DI / NLog In progress
Enforced configuration (JSON) Done
Split into WinApp + UI.Core Later

Done

Windows version flags removed

  • What: .NET 10 runs on Windows 10 1607 or Windows Server 2012 and later (Windows 8 level), so WinUtil.IsWindows9x / IsWindows2000 / IsWindowsXP (always false) and IsAtLeastWindows2000 / IsAtLeastWindowsVista / IsAtLeastWindows7 / IsAtLeastWindows8 (always true) are deleted and folded at about 40 call sites (app and TrlUtil): dead branches removed (Windows 9x user account check, XP sort arrows, pre-Vista DPI, task dialog and gradient fallbacks, Windows 98 PLGX compiler option, ...), Debug.Assert(<flag>) in catch blocks became Debug.Assert(false). Derived flags that became constant are gone too: UIUtil.VistaStyleListsSupported, ProtectedDialog.IsSupported, NativeProgressDialog.IsSupported (unused); stale commented-out code using them removed. IsAtLeastWindows10 stays (Server 2012 / 2012 R2 are older). Unused usings cleaned.
  • Caveat: Wine configured to report Windows XP or 7 now gets the Windows 8+ code paths.
  • Checks: build warnings unchanged; app and lib tests pass; the staged snapshot builds with the same errors as HEAD. Not run on Windows.

Comments about the .NET Framework updated

  • NOTIFYICONDATA_TIP_SIZE 64 → 128: WinForms' NotifyIcon.Text accepts 127 characters (MaxTextSize), the .NET Framework limit was 63; the tray tooltip (file path when locked) can now be longer.
  • ClipboardUtil.Clear and AceSecurity.UseClipboardViewerIgnoreFormat comments: WinForms uses OLE (OleSetClipboard, OleSetClipboard(null) to clear); the .NET Framework stored an empty DataObject.

Old .NET Framework checks and bug workarounds removed

  • What:
  • UIUtil.VistaStyleListsSupported: the Environment.Version.Major >= 2 part (always true).
  • DpiUtil.ConfigureProcess: the debug-only check of EnableWindowsFormsHighDpiAutoResizing in the .exe.config file (.NET Framework only; there is no such file on .NET 10) and the commented-out ConfigurationManager call.
  • CustomMessageFilterEx: the .NET 4.6 workaround that dropped WM_INPUTLANGCHANGEREQUEST with a handle outside the int range (InputLanguage.Culture overflowed). .NET WinForms takes the low word without an int conversion and tests it with 64-bit handles (PARAM.LOWORD); the now unused WM_INPUTLANGCHANGEREQUEST constant is gone too.
  • SiObf.GetRandomSeed: the .NET 2.0 guard against new Random(int.MinValue); checked on .NET 10.0.10, it works. Only changes the auto-type obfuscation sequence for that one seed.
  • NativeMethods.New.cs: the commented-out SetListViewGroupInfo / GetListViewGroupID (reflection) block; unused usings cleaned.
  • Checks: build warnings unchanged; app tests pass. Not run on Windows (input language switching and auto-type to check there).

ZipArchiveEx replaced with System.IO.Compression.ZipArchive

  • What: Util/Archive/ZipArchiveEx.cs (found ZipArchive by reflection, for .NET 4.0) deleted; the 1Password 1PUX import (its only user) uses ZipArchive directly. A missing entry (export.data or an attachment) is now a FormatException (invalid file), as the code intended; it was a FileNotFoundException before.
  • Checks: DataExchange/OnePw1Pux8Tests.cs (a 1PUX archive built in the test is imported: vault, entry fields, tags; an archive without export.data is a format error and imports nothing). Build warnings unchanged.

.NET Framework AppContext switches removed

  • What: Program.InitAppContext deleted. It set 18 "Switch.*" options of .NET Framework 4.6 to 4.8 through reflection. The only one .NET 10 still reads with another default, Switch.System.Drawing.Printing.OptimizePrintPreview (System.Drawing.Common, default false), is now set to true in KeePassCore.csproj (RuntimeHostConfigurationOption, written to KeePassCore.runtimeconfig.json).
  • Checks (under Wine, .NET 10.0.10): a throwaway probe searched the .NET 10 runtime assemblies for every switch name and ran the affected operations with each switch unset, on and off (icon with a PNG frame, paths over 260 characters, \\?\ paths, zip entry separator, default TLS protocols): same result in all cases. DontSupportPngFramesInIcons is still read but its default is the app value. The legacy accessibility and tooltip names only appear in a WinForms error message, not as switches. Build warnings unchanged; app tests pass.

KDB (KeePass 1.x) and KDBX 3.1 formats removed

  • What: the "KeePass KDB (1.x)" import / export (KeePassKdb1*.cs, about 1,300 lines) and the "KeePass KDBX (2.34, old format)" export (KeePassKdbx2v3, which forced KDBX 3.1). The file search no longer looks for .kdb files. The KeePass 1.x CSV and XML imports (text formats) stay; the CSV import keeps the KeePass 1.x "never expires" date (2999-12-28 23:59:59) as a local constant. 5 strings only used by these formats removed from DefaultText.xml and KPRes.Generated.cs (FormatNoDatabaseDesc, FormatNoDatabaseName, FormatOnlyOneAttachment, OldFormat, OptionReqOn). Unused usings left by the removal cleaned.
  • Why: KDBX 4.x only (project rule). A KeePass 1.x database must first be converted with KeePass 2.x or KeePassXC.
  • Checks: build warnings unchanged; app and lib tests pass. Not run on Windows.
  • Lib side: the lib writes KDBX 4 only; reading KDBX 3.1 is still in the lib (see the lib roadmap, Remove legacy code).

KeePassLibC removed (with the lib)

  • What: the "Use native library for faster key transformations" option is no longer shown (options and enforced options dialogs; AceNative.NativeKeyTransformations stays so existing configuration files still load, and is ignored); the About dialog no longer lists the native library; the plugin loader no longer skips its DLL names; MAX_PATH and INVALID_HANDLE_VALUE now live in the app's NativeMethods. Process protection: see To do 4.
  • Why: the DLL is not shipped, the lib no longer calls it.
  • Checks: build warnings unchanged; app tests pass. Not run on Windows.

Unix / Mono branches removed

  • What: every NativeLib.IsUnix() and MonoWorkarounds.IsRequired() (constant false on Windows) folded away; #if KeePassUAP / KeePassLibSD resolved; Unix-only files deleted (ClipboardUtil.Unix/Uwp, ClipboardU (xsel), SiEngineUnix (xdotool), NativeMethods.Unix, IpcBroadcast.Tcp/Fsw, the app MonoWorkarounds), KDE / GNOME font loading, Unix mutex files, the -wa-enable: / -wa-disable: options and the "force system font (Unix)" option.
  • Why: dead code on Windows (3,500 lines).
  • Decisions: AceUI.ForceSystemFontUnix kept (configuration files still load). The cross- process data protection (IPC files, clipboard) stays DPAPI: the app is Windows-only, so the planned cross-platform key file is not needed
  • Checks: no new compiler warning; build and tests.

Unix leftovers

  • What: Mono comments and commented-out Unix code; 8 KPRes strings only used by Unix code (DefaultText.xml + KPRes.Generated.cs, edited by hand the way TrlUtil would generate them); the hidden "hot keys help" link (SetupMono); the built-in kdbx URL override using mono (ID 0x2000 reserved: old configuration files may have it enabled); the Mono framework version detection (WinUtil); the xdotool XKeySym tables (SiCodes); a dead Mono bold / italic override (RichTextBuilder).
  • Kept on purpose: "Unix \n" newline comments, Linux notes in auto-type (they are about the target window, e.g. a Linux VM), AceUI.ForceSystemFontUnix.

DllImport → [LibraryImport]

  • What: 105 declarations (AllowUnsafeBlocks for the source generator).
  • Decisions:
  • Entry points are explicit ...W + UTF-16. CharSet.Auto already resolved to W. The former implicit A ones (SendMessage, PostMessage, MapVirtualKey) only carry messages above WM_USER or no text, and MapVirtualKey is only used with MAPVK_VK_TO_VSC: W is equivalent.
  • StringBuilder → char[] (NativeMethods.StringFromBuffer stops at the first \0 like a marshalled StringBuilder).
  • Structs: SHFILEINFO / PROCESSENTRY32 use fixed char buffers, ACTCTX / HDITEM use IntPtr strings. TaskDialogIndirect and EnumWindows keep their managed signatures through wrappers (StructureToPtr, function pointer).
  • Checks: struct sizes unchanged (64-bit values checked); a test checks that every declared export exists (Windows only, see Tests). Not run on Windows yet.

Images taken over from the lib

  • What: UI/GfxUtil.cs, UI/ScaleTransformFlags.cs, UI/CustomIconImages.cs (extension methods GetImage, IsImageValid, PwDatabase.GetCustomIcon, cache per icon object). ToSKColor() / ToPwColor() convert colors (SKColor.Empty <-> Color.Empty).
  • Bugs fixed on the way: ScaleImage returned the source image unscaled (and callers disposed it); HashImage64 always returned 0 (the banner cache mixed icons); an undecodable custom icon had no fallback image.
  • Checks: GfxUtilTests, CustomIconImagesTests, ColorConversionTests. The three bug tests were verified to fail with the bugs put back.

Smaller moves

  • NativeLib (process helpers) renamed KeePassCore.Util.ProcessUtil, with AppRunFlags moved from the lib; 10 unused using NativeLib = ... aliases removed. Why: two classes named NativeLib (app and lib) selected by aliases.
  • StrUtil.EncryptString / DecryptString moved from the lib as KeePassCore.Util.DpapiString (-pw-enc:, {PASSWORD_ENC}). Same format and domain separation tag as KeePass 2.x. Tests: DpapiStringTests (round trip and format: Windows only).
  • IUIOperations moved from the lib to KeePassCore.UI.
  • FileTransactionEx.ClearOld() call removed (TxF gone from the lib).
  • Weak key derivation dialog (KeyUtil.KdfAdjustWeakParameters): when the KDF is weak even with its defaults (AES-KDF, always weak now), "Yes" switches to KdfPool.GetDefaultParameters() (Argon2d) instead of the same KDF's defaults, which would be reported again at every open.
  • Lib delegates (GAction, GFunc, VoidDelegate, EntryHandler) replaced with Action / Func (see the lib roadmap).

Tests project

  • tests/KeePassCore.Tests: net10.0-windows with DisableTransitiveFrameworkReferences, so it runs without the Windows Desktop runtime, on Linux and macOS too. Rule: tests must not use WinForms types; Windows-only tests skip elsewhere.

Configuration in JSON

Settings were on XML format. Maintenance was not very easy, for the structure and the element parsing.
Going to a JSON format will be more easy to use (mapping to class, reading/Writing, ...

  • What: the configuration is JSON only. KeePass 2.x XML files are never read nor written.
  • Files: user %APPDATA%\KeePassCore\KeePassCore.config.json; global (portable) <exe name>.config.json next to the exe. The application data folder is KeePassCore, not KeePass, so nothing is shared with KeePass 2.x any more. Same precedence as before: the global file wins unless it prefers the user file.
  • Format (AppConfigJson): indented, enums as names, readable paths; reading accepts comments and trailing commas (hand-edited files). One object per section (Meta, Application, ..., Custom); a missing section gets its default values. Every value is written ([DefaultValue] / OmitItemsWithDefaultValues no longer mean anything). Serialization-only properties keep the short XML names (Triggers, Events, Conditions, Actions, Guid, TypeGuid, WorkingDirectories).
  • Two classes, one copy: AppConfiguration is the file content (plain JsonSerializer.Serialize / Deserialize); AppConfigEx (DI singleton) holds one AppConfiguration (Configuration) and its section properties (Meta, Application, ...) are shortcuts to it, so the 470 Program.Config.X call sites are unchanged. A file is read into a new object, which replaces the current one only if it was read. The constructor no longer loads; the configuration is loaded once in Program.CommonInitialize, where KeePass loaded it (AppConfigSerializer.Load(Config)), then OnLoad runs (credentials deobfuscated, relative paths, DPI scaling, search profiles). The object prepared for saving is the object written.
  • A file that cannot be read is kept: the error is shown, the defaults are used, and the configuration is not saved in this session (KeePass 2.x overwrote it at exit, with a backup in %TEMP%; that backup and its message are gone).
  • Removed: the second copy of the sections in AppConfigEx, the configuration types of the generated XmlSerializerEx (then removed entirely, see below), XmlUtil.Merge.cs (XML merge of the enforced file, with the plugin API MergeNodes), the XML path helpers, the [Xml*] attributes of the Ace* classes (the Ecas types keep theirs: copy / paste of triggers is still XML), the startup auto-disable of sections and AppConfigEx.Sections.cs (both back with the enforced configuration, see below), the -cfg-enf-setup-* options.
  • Fixed on the way (all from the half-done move):
  • the loaded configuration was thrown away (the app always ran on defaults), and the saved object was another copy;
  • OnLoad was no longer called: passwords stayed obfuscated in memory and the proxy credentials got one more layer of obfuscation at every save;
  • a load error wrote the defaults over the file, and the user file was %APPDATA%\KeePass\KeePass.config.xml, the KeePass 2.x file;
  • loading resolved AppConfigEx from DI inside its own constructor (recursion);
  • EcasCondition.Type had no [JsonIgnore]: a trigger with a condition broke the reload;
  • the enforced configuration, half removed, locked the UI: every list of the Options dialog and the Duplication dialog were read-only, the OK button of the URL overrides, triggers and password generator dialogs never closed them, and these dialogs changed the configuration even on Cancel (they edit a copy again; OK applies it);
  • a help URL from the user file was used: only an enforced one is (as in KeePass), so none for now;
  • the configuration error dialog built a new MainForm (Program.MainForm resolved a transient on every access): Program.MainForm is the running main window again;
  • a Debug.Assert on the built-in URL override flags failed at every Debug start (the reserved bit 0x2000 makes them non-contiguous): replaced with a test.
  • Checks: tests/KeePassCore.Tests/App/ConfigJsonTests.cs (round trip of the sections that load without WinForms, with a trigger condition, MRU credentials and properties, profiles, proxy; hand-edited file; built-in override flags). Under Wine: file written as above, a setting kept across runs, obfuscated proxy credentials unchanged after two saves, a broken file shown and left unchanged (no save), user file used when preferred, the KeePass 2.x files untouched.

XmlSerializerEx removed (translations use XmlSerializer)

  • What: .lngx translations are read and written with plain XmlSerializer (XmlUtilEx.Deserialize / Serialize, inside the same GZip stream): KPTranslation.Load / Save no longer take a serializer. Removed: XmlSerializerEx (reflection-based code generator, about 400 lines), its generated reader (pruned to the translation types in the JSON step), the -makexmlserializerex option, and IXmlSerializerEx in the lib.
  • Why: with the configuration in JSON, the generated reader only served translations. It was a .NET Framework speed trick and had to be regenerated by hand on Windows after any change of the translation types.
  • Cost: about 95 ms more, once, at startup and only when a language file is selected (German 2.57 under Wine, 3 runs: 36-44 ms generated vs 121-139 ms XmlSerializer).
  • Checks: the German 2.57 .lngx (15 tables, 1,237 strings, 53 forms, 869 controls) loaded with both readers gives byte-identical objects once written back. Under Wine, the app shows the German UI with that file (names renamed to KeePassCore.*, see Translations). Not covered by a test: the translation types use WinForms types, which the tests project cannot load.
  • Kept: KeePassCore.XmlSerializers.dll in the plugin loader's skip list (an old file left in the folder must not be loaded as a plugin).

Enforced configuration in JSON

Removed during the SkiaSharp migration, back in JSON.

  • File: <exe name>.config.enforced.json next to the exe (KeePassCore.config.enforced.json), same format as the configuration file (comments and trailing commas accepted). It is merged over the loaded configuration (global, user or defaults): objects property by property, enforced values win, an enforced list replaces the user list. A file that cannot be read is shown and enforces nothing (as KeePass).
  • Enforced options are read-only in the Options dialog (lists and single controls), the Duplication, Plugins and help source dialogs. AppConfigEx.IsOptionEnforced(object, property) finds the JSON path of the object in the configuration (EnforcedJson.GetPath, cached per object) and looks it up in the enforced file. The help URL and the plugin cache path are only used when enforced (as KeePass).
  • Triggers, URL overrides and password generator profiles (CVE-2023-24055, KeePass 2.54 rule): they are only used from the enforced file.
  • Their dialogs save into the enforced file (OK button with the shield). If it is not writable, the error says that an administrator must edit it, and the dialog stays open.
  • At startup, such items found in the user file are moved to the enforced file when it is writable (not when the global file prefers the user file), otherwise they are disabled, saved and listed in a message.
  • Writing: Tools → Advanced → Enforce Options (menu visible again) and the dialogs above write the file when the folder is writable (portable installation, administrator). No elevation (the KeePass 2.x UAC relaunch and its -cfg-enf-setup-* options are not back). The text that promised a UAC dialog (ConfigEnfAdminUac) now says that an administrator must edit the file. Writing rewrites the whole file: comments of a hand-written file are lost.
  • Code: EnforcedJson (merge, paths, set / remove of an option, WinForms-free), AppEnforcedConfig (file, checks, writing), AppConfigEx.Sections.cs (sections rule, from KeePass with a plain AppConfiguration as the values).
  • Checks: tests/KeePassCore.Tests/App/EnforcedJsonTests.cs (merge: enforced wins, lists replaced, enforced document not shared; a real AceSecurity section; enforced option or object; paths with JSON names and ignored properties; set and remove keep the file minimal). Under Wine: an enforced proxy address overrides the configuration file; an enabled trigger of the configuration file moves to the enforced file (writable folder) and stays enabled; with a read-only folder it is disabled, the message lists it and the user file is saved instead; once enforced, startup leaves the enforced file untouched and the enforced values are in the saved configuration.

To do

1. Run on real Windows

Nothing of the steps above has run on Windows yet (only Wine before them). Run the app and tests/KeePassCore.Tests (the export check and the DPAPI tests only run there), check auto-type, the clipboard, TaskDialog, file icons, IPC.

2. Rename to KeePassCore.WinApp

First step of the project renames (the lib takes the name KeePassCore afterwards).

  • Scope: folder, project, assembly (KeePassCore.WinApp.exe), root namespace KeePassCore.WinApp.* (needed: otherwise KeePassCore.Native would exist in two projects), tests project KeePassCore.WinApp.Tests, references (lib InternalsVisibleTo, TrlUtil, tests, KeePassCore.slnx).
  • Strings that must follow: translation keys ("KeePassCore.Forms.MainForm.m_menuMain", also in TrlUtil), "KeePassCore.Resources.KPRes" and the namespace in DefaultText.xml, KeePassCore.exe, KeePassCore.XmlSerializers.dll, the built-in kdbx override, START /MIN KeePassCore.exe.
  • Side effect: a global (portable) configuration file next to the exe becomes KeePassCore.WinApp.config.json; the user configuration in AppData keeps its name (KeePassCore\KeePassCore.config.json).
  • How (validated once, then reverted on request): apply the same path + content transformation twice, to HEAD's files (written to the index) and to the working tree, so work in progress is renamed too but stays unstaged. Simpler: do it right after a commit, on a clean tree.

3. Dependency Injection / NLog (draft)

I started to fully implement Dependency Injection in the WinApp. Because everything is a static class, and so nothing is really disposed until the app exit

  • MainForm is registered AddTransient; Program.MainForm is the running window again (set before Application.Run, null before), no longer a new form on every access (76 call sites). Register it as a singleton.
  • Dialogs: not singletons (a closed form cannot be shown again) and not plain transients from the root provider (it keeps every IDisposable it creates until exit). Use a factory (ActivatorUtilities.CreateInstance) or one scope per dialog.
  • host.RunAsync() is neither awaited nor stopped; Host.CreateDefaultBuilder adds unused configuration and providers. A plain ServiceCollection is enough until hosted services are needed.
  • appsettings.json is referenced in the csproj but does not exist.
  • Observed under Wine: the process does not exit after the main window is closed (killed after the timeout); probably the host started with RunAsync and never stopped.

4. Process protection: ProcessHardening.Apply() instead of AuxProtectProcessWithDacl

  • What: the "Protect process with DACL" option (AceSecurity.ProtectProcessWithDacl) called the native KeePassLibC DLL, which this fork does not ship: the option silently did nothing.
  • Done (first step): AuxProtectProcessWithDacl deleted from the lib; in Program.cs the call to ProcessHardening.Apply(ProcessHardeningOptions.DenyDebuggerAttach) from KeePassCore.Security.Memory (same idea: restrictive process DACL D:P(A;;GA;;;SY)(A;;0x00101001;;;OW)) is in place, commented: "uncomment and test on the last v3.x.x version". The option still does nothing until then.
  • To do: add the project reference, uncomment, test on real Windows.
  • Caveats: never run on real Windows; cannot be undone while the app runs; blocks your own debugger too (same as upstream).
  • Tests: to write, marked as not runnable for now: the measure is irreversible, so it needs a child process, on Windows.

5. Plugin support (PLGX)

Decision: PLGX stays until plugin support is redesigned. The PLGX compiler (CodeDom, not supported on .NET 10, about 1,700 lines) and WinUtil.GetMaxNetFrameworkVersion (registry NDP, only used by PLGX) are kept as they are. KeePass 2.x plugins do not load anyway (see the Plugins page).

6. Other

  • Database settings: the AES-KDF rounds field accepts 0 to 2^64 - 1, but the lib refuses to save outside 1 to 2^31 - 2 (AesKdf.MinRoundsForWrite / MaxRoundsForWrite, internal: visible to the app). Limit the field to that range.
  • Receive the remaining Windows-specific code of the lib once decided (CryptoUtil.ProtectData, possibly KcpUserAccount; see the lib roadmap). DPAPI can stay as is in this app.
  • Import / export formats (DataExchange): move UI-free formats (CSV, JSON, XML) to the lib or to UI.Core, so the CLI can use them.
  • Later: split the app in two projects. KeePassCore.WinApp (WinForms only) and KeePassCore.UI.Core (net10.0, no WinForms): what a frontend needs besides the lib, so that someone can build an Avalonia, MAUI or other UI on it (no other frontend is maintained here). Candidates (no direct System.Windows.Forms reference today, to confirm: some use it indirectly, e.g. through MessageService): configuration (15 of 18 files), DataExchange formats (61 of 68), triggers (Ecas, 21 of 24), placeholders (Util/Spr, 4 of 6), Util/Archive, GfxUtil / CustomIconImages, KPRes, IUIOperations. Needs interfaces for what the UI provides (messages, dialogs, clipboard, auto-type); goes with the DI work.

Decision log

Decision Why
Windows only, demo The goal is the lib; one can maintain frontend for Windows, other the CLI
DPAPI stays for IPC / clipboard / -pw-enc: Windows-only app; existing -pw-enc: strings keep working
SkiaSharp, not System.Drawing, for images System.Drawing (GDI+) is Windows-only; image code must be shareable with a future UI.Core library. Convert to Bitmap / Image only at the WinForms boundary (Extensions/ImageExtensions.cs). System.Drawing.Color (a plain struct) is fine
W entry points everywhere Checked equivalent for every former A call; Unicode like WinForms
Tests run without the Windows Desktop runtime Same suite on every OS; Windows-only checks skip
Rename includes the namespaces Avoids KeePassCore.* namespaces in two projects once the lib is renamed
Configuration JSON only, no KeePass 2.x XML import This app is a refactoring with its own files; KeePass 2.x keeps its XML, nothing is shared
A configuration file that cannot be read is never overwritten A hand edit with a typo must not cost every setting
Trust the user file for triggers / URL overrides / profiles until the enforced step The half-removed enforcement made them unusable; gap documented (CVE-2023-24055). Closed by the enforced step
Enforced file written by the app only when writable, no UAC elevation Windows-only relaunch never tested here; the administrator can always edit the JSON
An enforced list replaces the user list Simple and predictable; KeePass already replaced triggers, overrides and profiles