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...Wentry points withStringMarshalling.Utf16, noStringBuilder(char[]+NativeMethods.StringFromBuffer), blittable structs (otherwise wrap withMarshal.StructureToPtrlikeTaskDialogIndirect). - Images:
SkiaSharp. System.Drawing and then I would force window-only code again. WithSkiaSharpI 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) andIsAtLeastWindows2000/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>)incatchblocks becameDebug.Assert(false). Derived flags that became constant are gone too:UIUtil.VistaStyleListsSupported,ProtectedDialog.IsSupported,NativeProgressDialog.IsSupported(unused); stale commented-out code using them removed.IsAtLeastWindows10stays (Server 2012 / 2012 R2 are older). Unusedusings 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_SIZE64 → 128: WinForms'NotifyIcon.Textaccepts 127 characters (MaxTextSize), the .NET Framework limit was 63; the tray tooltip (file path when locked) can now be longer.ClipboardUtil.ClearandAceSecurity.UseClipboardViewerIgnoreFormatcomments: WinForms uses OLE (OleSetClipboard,OleSetClipboard(null)to clear); the .NET Framework stored an emptyDataObject.
Old .NET Framework checks and bug workarounds removed¶
- What:
UIUtil.VistaStyleListsSupported: theEnvironment.Version.Major >= 2part (always true).DpiUtil.ConfigureProcess: the debug-only check ofEnableWindowsFormsHighDpiAutoResizingin the.exe.configfile (.NET Framework only; there is no such file on .NET 10) and the commented-outConfigurationManagercall.CustomMessageFilterEx: the .NET 4.6 workaround that droppedWM_INPUTLANGCHANGEREQUESTwith a handle outside theintrange (InputLanguage.Cultureoverflowed). .NET WinForms takes the low word without anintconversion and tests it with 64-bit handles (PARAM.LOWORD); the now unusedWM_INPUTLANGCHANGEREQUESTconstant is gone too.SiObf.GetRandomSeed: the .NET 2.0 guard againstnew 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-outSetListViewGroupInfo/GetListViewGroupID(reflection) block; unusedusings 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(foundZipArchiveby reflection, for .NET 4.0) deleted; the 1Password 1PUX import (its only user) usesZipArchivedirectly. A missing entry (export.dataor an attachment) is now aFormatException(invalid file), as the code intended; it was aFileNotFoundExceptionbefore. - Checks:
DataExchange/OnePw1Pux8Tests.cs(a 1PUX archive built in the test is imported: vault, entry fields, tags; an archive withoutexport.datais a format error and imports nothing). Build warnings unchanged.
.NET Framework AppContext switches removed¶
- What:
Program.InitAppContextdeleted. 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 inKeePassCore.csproj(RuntimeHostConfigurationOption, written toKeePassCore.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.DontSupportPngFramesInIconsis 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.kdbfiles. 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 fromDefaultText.xmlandKPRes.Generated.cs(FormatNoDatabaseDesc,FormatNoDatabaseName,FormatOnlyOneAttachment,OldFormat,OptionReqOn). Unusedusings 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.NativeKeyTransformationsstays 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_PATHandINVALID_HANDLE_VALUEnow live in the app'sNativeMethods. 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()andMonoWorkarounds.IsRequired()(constantfalseon Windows) folded away;#if KeePassUAP/KeePassLibSDresolved; Unix-only files deleted (ClipboardUtil.Unix/Uwp,ClipboardU(xsel),SiEngineUnix(xdotool),NativeMethods.Unix,IpcBroadcast.Tcp/Fsw, the appMonoWorkarounds), 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.ForceSystemFontUnixkept (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-inkdbxURL override usingmono(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 (
AllowUnsafeBlocksfor the source generator). - Decisions:
- Entry points are explicit
...W+ UTF-16.CharSet.Autoalready resolved toW. The former implicitAones (SendMessage,PostMessage,MapVirtualKey) only carry messages aboveWM_USERor no text, andMapVirtualKeyis only used withMAPVK_VK_TO_VSC:Wis equivalent. StringBuilder→char[](NativeMethods.StringFromBufferstops at the first\0like a marshalledStringBuilder).- Structs:
SHFILEINFO/PROCESSENTRY32usefixed charbuffers,ACTCTX/HDITEMuseIntPtrstrings.TaskDialogIndirectandEnumWindowskeep 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 methodsGetImage,IsImageValid,PwDatabase.GetCustomIcon, cache per icon object).ToSKColor()/ToPwColor()convert colors (SKColor.Empty<->Color.Empty). - Bugs fixed on the way:
ScaleImagereturned the source image unscaled (and callers disposed it);HashImage64always 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) renamedKeePassCore.Util.ProcessUtil, withAppRunFlagsmoved from the lib; 10 unusedusing NativeLib = ...aliases removed. Why: two classes namedNativeLib(app and lib) selected by aliases.StrUtil.EncryptString/DecryptStringmoved from the lib asKeePassCore.Util.DpapiString(-pw-enc:,{PASSWORD_ENC}). Same format and domain separation tag as KeePass 2.x. Tests:DpapiStringTests(round trip and format: Windows only).IUIOperationsmoved from the lib toKeePassCore.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 toKdfPool.GetDefaultParameters()(Argon2d) instead of the same KDF's defaults, which would be reported again at every open. - Lib delegates (
GAction,GFunc,VoidDelegate,EntryHandler) replaced withAction/Func(see the lib roadmap).
Tests project¶
tests/KeePassCore.Tests:net10.0-windowswithDisableTransitiveFrameworkReferences, 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.jsonnext to the exe. The application data folder isKeePassCore, notKeePass, 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]/OmitItemsWithDefaultValuesno longer mean anything). Serialization-only properties keep the short XML names (Triggers,Events,Conditions,Actions,Guid,TypeGuid,WorkingDirectories). - Two classes, one copy:
AppConfigurationis the file content (plainJsonSerializer.Serialize/Deserialize);AppConfigEx(DI singleton) holds oneAppConfiguration(Configuration) and its section properties (Meta,Application, ...) are shortcuts to it, so the 470Program.Config.Xcall 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 inProgram.CommonInitialize, where KeePass loaded it (AppConfigSerializer.Load(Config)), thenOnLoadruns (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 generatedXmlSerializerEx(then removed entirely, see below),XmlUtil.Merge.cs(XML merge of the enforced file, with the plugin APIMergeNodes), the XML path helpers, the[Xml*]attributes of theAce*classes (theEcastypes keep theirs: copy / paste of triggers is still XML), the startup auto-disable of sections andAppConfigEx.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;
OnLoadwas 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
AppConfigExfrom DI inside its own constructor (recursion); EcasCondition.Typehad 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.MainFormresolved a transient on every access):Program.MainFormis the running main window again; - a
Debug.Asserton the built-in URL override flags failed at every Debug start (the reserved bit0x2000makes 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:
.lngxtranslations are read and written with plainXmlSerializer(XmlUtilEx.Deserialize/Serialize, inside the same GZip stream):KPTranslation.Load/Saveno 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-makexmlserializerexoption, andIXmlSerializerExin 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 toKeePassCore.*, see Translations). Not covered by a test: the translation types use WinForms types, which the tests project cannot load. - Kept:
KeePassCore.XmlSerializers.dllin 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.jsonnext 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 plainAppConfigurationas the values). - Checks:
tests/KeePassCore.Tests/App/EnforcedJsonTests.cs(merge: enforced wins, lists replaced, enforced document not shared; a realAceSecuritysection; 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 namespaceKeePassCore.WinApp.*(needed: otherwiseKeePassCore.Nativewould exist in two projects), tests projectKeePassCore.WinApp.Tests, references (libInternalsVisibleTo, TrlUtil, tests,KeePassCore.slnx). - Strings that must follow: translation keys (
"KeePassCore.Forms.MainForm.m_menuMain", also in TrlUtil),"KeePassCore.Resources.KPRes"and the namespace inDefaultText.xml,KeePassCore.exe,KeePassCore.XmlSerializers.dll, the built-inkdbxoverride,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
MainFormis registeredAddTransient;Program.MainFormis the running window again (set beforeApplication.Run,nullbefore), 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
IDisposableit creates until exit). Use a factory (ActivatorUtilities.CreateInstance) or one scope per dialog. host.RunAsync()is neither awaited nor stopped;Host.CreateDefaultBuilderadds unused configuration and providers. A plainServiceCollectionis enough until hosted services are needed.appsettings.jsonis 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
RunAsyncand never stopped.
4. Process protection: ProcessHardening.Apply() instead of AuxProtectProcessWithDacl¶
- What: the "Protect process with DACL" option (
AceSecurity.ProtectProcessWithDacl) called the nativeKeePassLibCDLL, which this fork does not ship: the option silently did nothing. - Done (first step):
AuxProtectProcessWithDacldeleted from the lib; inProgram.csthe call toProcessHardening.Apply(ProcessHardeningOptions.DenyDebuggerAttach)fromKeePassCore.Security.Memory(same idea: restrictive process DACLD: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, possiblyKcpUserAccount; 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 toUI.Core, so the CLI can use them. - Later: split the app in two projects.
KeePassCore.WinApp(WinForms only) andKeePassCore.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 directSystem.Windows.Formsreference today, to confirm: some use it indirectly, e.g. throughMessageService): configuration (15 of 18 files),DataExchangeformats (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 |