Resolve conflicts after master gained the flat-file database backend,
AI client, and other cproof-fork features:
- CHANGELOG: keep cproof-fork unreleased section atop full upstream
history (0.17.0 / 0.16.0 / 0.15.1)
- src/command/cmd_ac.c: keep both upstream spellcheck AC and cproof
/history switch|verify|export|import autocompletion
- src/config/preferences.c: merge upstream [spellcheck] group with the
cproof [ai]/[ai/<provider>] groups
- src/database.c: take master (thin dispatcher); upstream SQLite
improvements (v3 migration, auto_gchar) belong in database_sqlite.c
and will land in a follow-up commit
- src/tools/autocomplete.c: take upstream unescape of search_str
- src/ui/statusbar.c: keep guint signatures (per project preference)
and adapt _status_bar_draw_dbbackend to guint as well
- src/xmpp/jid.c: combine the new jid_is_valid/jid_is_valid_user_jid
with the "(null)" legacy-placeholder guard in
jid_create_from_bare_and_resource; use auto_gchar
- tests/functionaltests/proftest.c: keep unsigned-safe pre-check on
output_len before subtracting OUTPUT_BUF_SIZE
- tests/unittests/unittests.c: keep upstream subdirectory include
layout plus cproof-only test_ai_client/database_export/database_stress
- tests/unittests/test_jid.c: drop orphan top-level copy (the
restructured version lives in tests/unittests/xmpp/test_jid.c)
Upstream is included up to 1ac05754b (release 0.18.0 era). Follow-up:
pull 1ac05754b..upstream/master (esp. 42a849d16 db migration fix and
4749645c7 receipt-request restore), and adapt the SQLite improvements
into database_sqlite.c.
_win_printf dropped buffer append and render for any window in paged
state, but only WIN_CHAT can recover lost messages via chatwin_db_history()
on scroll-down. WIN_MUC, WIN_PRIVATE and WIN_AI have no such fallback, so
incoming and outgoing messages were silently lost when the user was viewing
history.
Introduce log_database_can_recover_messages() to check whether the DB
backend can replay messages (returns FALSE when PREF_DBLOG is "off",
"redact", or no backend is active). Gate the WIN_CHAT early return in
_win_printf() on this check: when recovery is impossible, fall through
and append to the buffer so messages remain visible on scroll-down.
Add a buffer-bottom reset in win_page_down() for non-chat windows.
WIN_SCROLL_REACHED_BOTTOM is only set on the is_chat DB branch, so
non-chat windows never clear paged on their own; reset paged and
unread_msg when the last line of the buffer reaches the screen.
Add missing scroll rendering for AI windows in the title bar draw
function, ensuring AI windows display their scrolled state consistently
with other window types.
Remove the manual paged/unread_msg reset before printing the user
message in cl_ev_send_ai_msg() -- it was a local workaround for the
same drop and is no longer needed.
Deprecate /logging command in favor of /history for chat logging control.
/history off now stops persistence (sets PREF_DBLOG=off + PREF_CHLOG=false)
in addition to hiding history on open. /history on restores persistence
(re-enabling PREF_DBLOG=on if it was off) and PREF_CHLOG.
statusbar
- PREF_STATUSBAR_SHOW_DBBACKEND (default ON) gates the [sqlite] /
[flatfile] indicator; toggle via "/statusbar show|hide dbbackend"
/history off|on
- "/history off" now stops persistence as well as hiding history on
open: sets PREF_DBLOG=off + PREF_CHLOG=false in addition to
PREF_HISTORY=false
- "/history on" restores persistence (re-enabling PREF_DBLOG=on if
it was off) and PREF_CHLOG, in addition to PREF_HISTORY=true
/logging
- Deprecated /logging command. It now prints a single notice
pointing to /history; CMD_PREAMBLE trimmed (min_args=0, no
setting_func, syntax/args/examples dropped); subcommand 'group'
removed from autocomplete
- All "use '/logging chat on' to enable" hints replaced with
"/history on" across /omemo-log, /pgp-log, /otr-log, and /ox-log
/privacy logging
- Single-pass validation across {on, off, redact, flatfile}
- Backend-switching values (on, flatfile) now take effect
immediately when connected (via log_database_switch_backend),
matching "/history switch" behaviour; off/redact only flip
pref bits and keep the live backend open
/correction off
- win_print_outgoing and win_print_outgoing_with_receipt now gate
_win_correct on PREF_CORRECTION_ALLOW, matching incoming-msg
paths. Previously a peer's correction reflected via XEP-0280
carbons (or the user's own /correct invocation) was applied
in-buffer regardless of the pref
console
- Drop orphaned /logging chat reference from cons_privacy_setting()
- Delete cons_logging_setting() and remove its call from
cons_show_log_prefs()
autocomplete
- Add dbbackend to statusbar_show_ac
- Remove logging_ac entries for chat/group subcommands
- Remove _logging_autocomplete() param handler for chat subcommand
database_flatfile
- Two local g_strndup allocations switched from char* with manual
g_free to auto_gchar gchar* for automatic cleanup
tests
- Update test_cmd_otr.c to expect new /history-on warning messages
@author: jabber.developer2 <jabber.developer2@jabber.space>
Add missing `ai_session_unref` calls to prevent memory leaks.
- In `_ai_request_thread`, release session on error paths and
after successful processing.
- In `cmd_ai_start`, release reference after passing ownership
to the AI window.
Replace manual g_free calls with auto_gchar for local_provider_name,
local_model, local_api_key, and response_data to ensure automatic
cleanup and prevent memory leaks.
Add an AI client module that integrates with OpenAI-compatible API
providers (OpenAI, Perplexity, and custom endpoints) to provide
AI-assisted chat within CProof. Users can start sessions with /ai start,
send prompts, receive responses in a dedicated AI window, switch between
providers and models, and manage API keys — all with tab-completion.
Providers are configured via /ai set commands with per-provider API keys,
endpoints, default models, and custom settings. Two default providers
(openai, perplexity) are seeded on first use. Provider state persists in
[ai/<name>] sections of the preferences keyfile with automatic migration
from the previous flat-key format.
The /ai command integrates into the existing command system with 8
subcommands covering provider management, session lifecycle, model
fetching, and conversation clearing. Autocomplete uses the standard
flat prefix-matching chain for reliable tab-completion at every nesting
level. A new ProfAiWin window type is added to the window system.
Architecture:
Async design: HTTP requests run on a background thread (pthread) to avoid blocking the ncurses UI loop; results are displayed on the main thread via direct function calls
Thread safety: AIProvider and AISession use atomic ref-counting and mutex-protected session state; the request thread snapshots all session data before making the HTTP call
Window validation: wins_ai_exists() prevents use-after-free when the user closes the AI window during an in-flight HTTP request (~60s)
Privacy: store:false is sent with every request to prevent providers from persisting conversations or using them for training
Response size limit: 10MB cap with immediate curl abort via CURL_WRITEFUNC_ERROR to prevent OOM
JSON parsing uses unified helpers for both chat responses and error
envelopes with consistent escape decoding. The response parser tries
Perplexity /v1/responses "text" field first, then falls back to OpenAI
"content". Error parsing extracts provider error.message from the
standard envelope format. Model parsing handles multiple API response
formats (OpenAI list, Perplexity, array) including edge cases.
Tests include 470+ lines of unit tests covering provider management,
session lifecycle, JSON parsing (multiple formats), autocomplete cycling,
and error handling, plus functional tests for /ai command dispatch.
A stub_ai.c module isolates unit tests from UI dependencies.
A flat-file alternative to the SQLite chatlog backend with runtime
switching, full migration tooling, integrity verification, and a
synthetic load harness. SQLite remains the default; both backends share
one dispatch layer (db_backend_t vtable) so callers don't change.
Storage layout
- Per-contact append-only `flatlog/<account>/<contact>/history.log`
under XDG_DATA_HOME, one line per message
- Single-line file header with embedded format-version marker
(FLATFILE_FORMAT_VERSION); reader warns on missing or mismatched
marker, writer and checker stay in sync via preprocessor
stringification
- Deterministic key=value metadata (`id`, `aid`, `corrects`, `to`,
`to_res`, `read`) plus escaped body \u2014 `\|`, `\]`, `\\`, `\n`, `\r`
literals prevent log injection
- Sparse byte-offset index (FF_INDEX_STEP=500) per contact for
O(log n) time-range lookups; rebuilt on inode / size / mtime
change, extended in-place when the file just grew
- Per-contact GHashTable caches for archive_id presence and
stanza_id \u2192 from_jid mapping (O(1) MAM dedup, O(1) LMC sender
validation)
Hardening
- Path-traversal protection: JID directory name normalisation
(`@` \u2192 `_at_`, slashes and `..` rejected at construction); every
per-contact path is anchored under the account's flatlog/
directory and validated before open
- Symlink-attack protection: every fopen / open uses O_NOFOLLOW; on
ELOOP the operation aborts with an error rather than following
- Filesystem permissions: log files created with mode 0600,
directories with mode 0700; both enforced at creation, verified
on each open and reported on drift by `/history verify`
- Atomic crash-safe export: write to a temp file via mkstemp (mode
0600, random suffix, no name collisions between concurrent
exports), fsync, then rename \u2014 partial state never replaces the
live file
- Concurrency: advisory flock(LOCK_EX) held for the duration of
every write, including append from live messages and full rewrite
from export, so two profanity processes can't interleave bytes
on the same log
- DoS / abuse guards:
* FF_MAX_LINE_LEN = 10 MB \u2014 lines longer than this are rejected
at read with a warning; the parser will not allocate
unbounded memory for a single record
* FF_MAX_LMC_DEPTH = 100 \u2014 `corrects:` chain walk stops at this
depth and emits a warning, preventing a malicious correction
cycle from spinning the apply pass
* FF_VERSION_SCAN_MAX = 16 \u2014 header version probe never reads
past 16 leading comment lines, even on garbage input
* Empty / inverted byte-range early-return in page-up read path
so a malformed time filter cannot cause an unbounded scan
* Zero-entry index guard so a file whose every line failed to
parse cannot cause a NULL deref on later page-up
- LMC sender validation: an incoming correction whose sender does
not match the original message's sender is rejected at write
time and surfaced via cons_show_error; a cycle in the apply pass
is broken via a visited-set
- jid_create_from_bare_and_resource treats NULL, empty string, and
the literal "(null)" as no resource and returns a bare jid;
similar normalisation for barejid eliminates the legacy
"user@host/(null)" artefact that leaked into stored fulljids
whenever g_strdup_printf("%s", NULL) ran inside create_fulljid
Commands
- `/history switch sqlite|flatfile` \u2014 runtime backend swap, closes
the old backend and opens the new one without reconnecting
- `/history export [<jid>]` \u2014 SQLite -> flat-file, merging with any
existing flatlog (dedup keyed on a SHA-256 hash mixing stanza_id,
timestamp, from_jid, body \u2014 robust against id reuse by older
clients)
- `/history import [<jid>]` \u2014 flat-file -> SQLite, same merge
semantics, runs inside a single SQLite transaction with rollback
on per-contact failure
- `/history verify [<jid>]` \u2014 integrity check; emits a structured
list of issues (ERROR / WARNING / INFO) per file:
* file-level: missing log, wrong permissions (\u2260 0600), UTF-8
BOM present, CRLF line endings, empty file
* line-level: invalid UTF-8 (with byte offset), embedded
control characters, unparsable lines, timestamps out of
order, duplicate `id:` and `aid:` (tracked separately so a
stanza/archive id collision isn't double-reported)
* cross-line: broken `corrects:` references whose target id is
not present in the file
- `/history backend` \u2014 show currently active backend
- Active backend indicator `[sqlite]` / `[flatfile]` in the status
bar next to the JID
- Roster-JID autocomplete for verify / export / import
- export and import open a SQLite handle on demand when the
flatfile backend is currently active, so migration works
regardless of which backend is live
Tests
- Unit: database_export (parser round-trip, escape/unescape, dedup
key stability, JID normalisation), database_stress (14 cases
exercising rapid writes, large messages, deep LMC chains, MAM
dedup, concurrent contacts)
- Functional: history persistence across reconnects, export /
import round-trip with content equality, MUC migration,
timestamp normalisation across timezones
- Bench harness P1\u2013P5 (synthetic load: bulk insert, time-range
read, page-up scroll, MAM ingest, mixed workload) and failure
modes F1\u2013F17 (page-up cursor and forward-iteration symmetry,
oversized lines, MAM dedup, LMC depth and cycles, BOM/CRLF,
missing log, empty file, mtime+inode flip, broken corrects, etc.)
- All bench tests integrate with the existing make targets and
emit CSV rows for baseline comparison
Author: jabber.developer2 <jabber.developer2@jabber.space>
Reviewed-by: jabber.developer <jabber.developer@jabber.space>
- ui/window.c win_show_subwin (#1144): unconditional newpad without
freeing the previous pad leaks the ncurses subwin if the function
is invoked twice without an intervening win_hide_subwin. Added a
delwin guard.
- omemo/omemo.c omemo_on_disconnect (#1126): hash tables and
libsignal handles are destroyed unconditionally even when a full
init never ran (e.g. shutdown after a failed load), and neither
g_hash_table_destroy nor signal_*_destroy are documented as
NULL-safe. NULL-check each handle before tearing it down.
- xmpp/roster_list.c (#1140 / #1141 / #1160): null the caller's
pointer immediately after resource_destroy() so a stale reference
cannot trigger a double-free; resource_destroy itself only zeroes
the struct fields it owns and cannot reach back to the caller.
Cleanup of the conversion-safety warnings exposed by enabling
-Wconversion / -Wsign-compare in the previous commit, plus guard
clauses at the few places where unsigned arithmetic could actually
misbehave.
Build:
- configure.ac drops -Wno-error=conversion and
-Wno-error=float-conversion. Only -Wno-error=sign-conversion and
-Wno-error=sign-compare remain, gating the ~230 sign warnings
inherited from upstream that will be cleaned up in follow-ups.
Type / conversion fixes (no behaviour change):
- Length-like locals in command/cmd_ac.c, command/cmd_funcs.c,
pgp/gpg.c, tools/autocomplete.c, tools/parser.c and ui/mucwin.c
switched from int to size_t / glong (matching strlen /
g_utf8_strlen return type) so we no longer need an (int) cast and
loop counters / array sizes stay in their natural unsigned domain.
- g_timer_elapsed / GTimeSpan -> int casts in session.c, iq.c,
core.c, server_events.c, window.c.
- _win_print_wrapped: indent parameter and local curx/maxx switched
from size_t to int to match _win_indent / getcurx / getmaxx.
- Port casts (int -> unsigned short) at the libstrophe boundary in
connection.c and session.c, each preceded by
g_assert(port >= 0 && port <= UINT16_MAX) so the truncation is
documented at the call-site.
- curl_off_t / fread size_t results cast at usage in http_upload.c,
http_download.c, omemo/crypto.c.
- strtoul results cast to uint32_t in xmpp/omemo.c and omemo/omemo.c
where device/prekey IDs are genuinely 32-bit.
- config/color.c: fg/bg/palette indices switched to `short`
end-to-end (find_col, color_hash, find_closest_col,
_color_pair_cache_get, cache.pairs), so the ncurses init_pair
boundary needs at most one (short)i cast for the cache index. Also
TODO-noted: init_extended_pair is needed for >15-bit palettes.
- xmpp/avatar.c: float arithmetic explicitly casts its int operands.
- tests/functionaltests/proftest.c: read() result handling uses
size_t for the accumulator, _read_output returns ssize_t, and the
buffer-shift check happens before space subtraction so the
expression cannot underflow.
Real-risk guard clauses (the part that actually fixes bugs):
- src/ui/statusbar.c _tabs_width: `end > opened_tabs - 1` rewritten
as `end < opened_tabs` so opened_tabs == 0 no longer underflows.
- src/ui/statusbar.c _status_bar_draw_extended_tabs: the mirror
comparison rewritten as `end >= opened_tabs`.
- src/ui/statusbar.c status_bar_draw: replaced
`MAX(0, getmaxx - (int)_tabs_width)` with an explicit precheck
before subtraction.
- src/omemo/omemo.c prekey selection: prekey_index is now uint32_t
and randomized into an unsigned buffer, so modulo with prekeys_len
cannot yield a negative index for g_list_nth_data.
- src/omemo/crypto.c omemo_decrypt_func: PKCS#5/PKCS#7 unpadding
reads `plaintext[plaintext_len - 1]`, which would underflow on a
malformed empty ciphertext and read past the heap buffer. Reject
plaintext_len == 0 before the padding peek and validate the
padding byte against the buffer length before the unpad loop.
Initialise plaintext = NULL so the early `goto out` cannot free
uninitialised memory.
- src/ui/inputwin.c (4 mbrlen sites) and src/ui/window.c
_win_print_wrapped: mbrlen() returns 0 for the null wide
character. The existing checks rejected (size_t)-1 / -2 but
treated 0 as a valid step, so the surrounding loops would either
advance by SIZE_MAX (i += ch_len - 1) or spin in place
(word_pos += 0 forever). Add `|| ch_len == 0` to each guard;
inside the spell-check word-emission loop also fall back to a
one-byte advance.
- Defensive `len > 0 ? len - 1 : 0` prechecks at the strlen-based
g_strndup / loop sites in ui/console.c, plugins/c_api.c and
plugins/python_plugins.c.
Turn on the stricter conversion-safety warnings and wire in an optional
sanitizer build mode. Nothing is promoted to a hard error yet: the
warnings become part of the build output and can be cleaned up
incrementally without blocking development.
- Drop -Wno-sign-compare and add -Wsign-compare + -Wconversion to the
GCC-specific warnings loop (probed for compiler support).
- Add --enable-sanitizers flag that enables ASan + UBSan +
-fsanitize=unsigned-integer-overflow with -fno-sanitize-recover=all.
Off by default; intended for a dedicated CI job.
- Under PACKAGE_STATUS=development (-Werror), opt out of turning the
new conversion/sign-compare warnings into hard errors via
-Wno-error=sign-conversion/-conversion/-float-conversion/-sign-compare.
Existing codebase has ~240 sign-conversion, ~87 other narrowing and a
handful of sign-compare warnings (mostly inherited from upstream);
they stay visible but don't block development builds.
- Route glib/gio CFLAGS through -isystem so macros like
GPOINTER_TO_UINT in glibconfig.h don't generate noise.
Arch Linux ships a stripped ld-linux-x86-64.so.2, so Valgrind cannot
find the memcmp/memcpy/strlen symbols it must redirect and exits at
startup with "Fatal error at startup: a function redirection which is
mandatory for this platform-tool combination cannot be set up".
debuginfod is already installed in the image but was never wired up.
Set DEBUGINFOD_URLS to the official Arch debuginfod server so Valgrind
fetches the missing glibc debug symbols on demand and caches them.
Apply fixes, refactors and test additions requested by reviewer on PR #105.
Fixes:
- database: warn and notify user on duplicate archive_id instead of
silently debug-logging it (R05).
- database: add missing '[' in "[DB Migration]" log prefix (R06).
- xmpp/resource: NULL-out name/status after g_free to avoid double-free
via roster_list.c cleanup path (R09, R23).
- common: widen strtoi_range internal storage from int to long so that
values in (INT_MAX, LONG_MAX] are rejected as out-of-range instead of
being silently truncated on 64-bit platforms (R25).
Refactors:
- xmpp/message: extract _receive_omemo helper, removing three copies of
the OMEMO receive block in groupchat / MUC-PM / chat handlers (R04).
- omemo: flatten deeply nested device-list processing via guard-clause
continues (R11).
- tools/autocomplete: merge two nested ifs into a single && condition
(R13).
- ui/titlebar: extract _show_trust_indicator and inline _wprintw_withattr
wrapper, collapsing three near-identical trust-indicator blocks (R22).
- config/tlscerts: drop _checked_g_strdup wrapper; g_strdup is
NULL-safe per glib documentation (R19).
- ui/inputwin: use auto_gchar for spellcheck word instead of manual
g_free (R20).
- tools/editor: drop outdated "Deprecated synchronous" comment that
no longer matches the callback-based implementation (R28).
Tests:
- tests/command/cmd_ac: rename segfaults_when_empty ->
no_segfault_when_empty; expand cycling coverage to three files plus
backward SHIFT-TAB traversal (R16, R17).
- tests/common: add strtoi_range overflow/underflow and strtol-parsing
consistency tests (R25).
- tests/xmpp/jid: add test for '@' inside resourcepart per RFC 6122
section 2.4 (R26).
Misc:
- xmpp/omemo: change omemo_error_to_string return type from char* to
gchar* for glib consistency (R01).
- subprojects/libstrophe: point wrap-git at our fork at
git.jabber.space/devs/libstrophe-gh (R24).
- RELEASE_GUIDE: drop "Updating website" section referring to an
upstream site that is not ours (R18).
Inject system flags from /etc/makepkg.conf into the CI environment to
detect build collisions caused by Pikaur's configuration bug.
Pikaur's cascading logic causes flags from /etc/makepkg.conf to be
merged into the build environment. This creates collisions with flags
defined in the project's Makefile.am (e.g., duplicate -D_FORTIFY_SOURCE
definitions), which can cause builds to fail for users.
By exporting these flags in the CI environment, we ensure that any
code change that is sensitive to flag duplication will trigger a
failure in our Arch Linux CI matrix, preventing broken builds from
reaching users.
Implementation details:
- Detects Arch Linux via /etc/os-release.
- Uses a sed-based flattener to handle multi-line variables and
trailing backslashes in makepkg.conf.
- Exports the flags to the shell environment so that 'configure'
and 'make' inherit them naturally, maintaining parity with a
real Pikaur session.
Updated tooling builds supported XEP list from profanity.doap file
The profanity.doap file is a git submodule in the website repository.
It is used as an input to create the supported XEPs webpage.
3cd439d172Fixes: #2154
Signed-off-by: Benson Muite <benson_muite@emailplus.org>
We recently made the jid validation and creation functions more strict.
So this is a sideffect of it. We need to use `jid_fulljid_or_barejid`
instead of jidp->fulljid here since it now treats just the barejid not
as a fulljid.
Ref: 09757da5df
Ref: f251cc8bb3
Ref: fa64b135df
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
If we build Profanity without GTK we dont iterate the context so we will
never know when the editor exited. Calling `g_main_context_iteration()`
or finally switching to GMainLoop fixes this.
So far calling `tray_update()` does this for us in GTK builds.
So this went unnoticed.
The editor will inherit ignored signal handlers (SIGINT, SIGTSTP, SIGPIPE)
from us. Neovim uses libuv which seems to check SIGINT on startup to
determine whether the environment is interactive or not.
We now reset these signals to SIG_DFL in the child process before execvp.
Profanity uses readline which still competed for terminal input with the
editor. We only took care about other Profanity UI processes in our
previous commit. This led to misordered characters in the editor.
In my tests with vim everything worked fine and these bugs were
discovered when a user used neovim.
Fixes: https://github.com/profanity-im/profanity/issues/2148
Ref: 36ec2b0ae1
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
For key transport messages, Monal at least doesn't append the
authentication tag to the plaintext key contents as that only makes
sense if the key was used to encrypt something. This causes the key
length check to fail and show the
OMEMO message received but decryption failed.
error to the user which is confusing because there is no user-originated
message involved.
Skip the length check for key transport messages as profanity only uses
these to advance the ratchet and makes no use of the decrypted contents.
Signed-off-by: Karel Balej <balejk@matfyz.cz>
ASan and Valgrind both intercept memory allocations and management at
runtime. Running them simultaneously might lead to conflicts in memory
tracking.
This change ensures that during the Valgrind phase of the build matrix,
only Valgrind is responsible for memory analysis, avoiding redundant
overhead and ensuring more reliable results.
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
Remove IDE specific entries, they belong in the global conf of the user.
Remove autotools specific files.
Ref: bc777c56b2
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
Move from blocking fork/wait logic to nonblocking fork and
g_child_watch_add.
This ensures that the Profanity main loop continues to run while an
external editor is open. So we don't loose connection and react to
pings.
We change editor handling also in vcard and muc subject editing.
In the new implementation we are launching the editor and passing a
callback which we will use once the editor exited.
We use the recently added ui_susped() and ui_resume().
To not clutter the UI we need to check whether Profanity UI is suspended
and omit drawing in this case.
Fixes: https://github.com/profanity-im/profanity/issues/1888
Ref: 9b112904a9bc7250dc013d901187ca8622580d98
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
There was way too much repetition here.
This is in preparation for future changes regarding the editor.
Signed-off-by: Michael Vetter <jubalh@iodoru.org>
Commit 81f92f1fe introduced a selective update mechanism to
_accounts_save() to prevent overwrites when running multiple instances
by only modifying the specific account being saved.
Commit 5c484c fixed a problem with that commit regarding empty accounts.
It turns there was another bug introduced:
The new implementation only set the keys that existed in memory.
So removal of keys was not possible anymore.
Fixes: 81f92f1fed
Ref: 5c484c26ed
Signed-off-by: Michael Vetter <jubalh@iodoru.org>