Skip to content

Backup silently corrupts all servers/keys since #1196 — custom toEncodable disables .toJson(), objects serialized via toString() #1223

Description

@PeterSpiegler

Title: Backup silently corrupts all servers/keys since #1196 — custom toEncodable disables .toJson(), objects serialized via toString()


Describe the bug

Since PR #1196 (first shipped in v1448), BackupV2.backup() passes a custom toEncodable to json.encode:

// lib/data/model/app/bak/backup2.dart:117
var result = json.encode(bak.toJson(), toEncodable: _toEncodable);

In Dart, providing a custom toEncodable replaces the encoder's default behavior of calling .toJson() on non-primitive objects. The fallback then stringifies everything that is not an Enum:

// lib/data/model/app/bak/backup2.dart:141-145
Object? _toEncodable(Object? value) {
  if (value is Enum) return value.name;
  _loggerV2.warning('Non-JSON-encodable type: ...');
  return value.toString();
}

Entries stored in Hive as typed objects (Spi, PrivateKeyInfo, snippets — via the adapters in lib/hive/hive_registrar.g.dart) end up in the backup as strings like "Spi<user@host:22>" instead of JSON objects, losing all credentials/settings at backup time.

On restore, these strings are dropped silently:

// lib/data/store/cached_store.dart:84-95 (_getAndConvert)
if (raw is Map) {
  try {
    final item = fromJson(Map<String, dynamic>.from(raw));
    ...
  } catch (e) {
    dprint('Parsing $T from JSON', e);   // debug-only
  }
}
return null;   // String entries are skipped without any user-visible error

WebDAV/iCloud sync uses the same BackupV2 path (lib/core/sync.dart) and is equally affected.

To Reproduce

  1. On v1448+ (tested with v1450), have servers stored (create some, open the server list once)
  2. Go to Settings → Backup, create a backup (file or WebDAV)
  3. Restore this backup on another device (or after clearing data)
  4. See empty server list, keys, and snippets — without any error message

Desired Results

Backup contains all servers/keys/snippets as JSON objects; restore brings them back. If serialization or parsing of an entry fails, the user is informed instead of data being dropped silently.

Actual Results

Every entry under spis/keys in the backup is a String (e.g. "Spi<user@host:22>"), not a JSON object — verified by decrypting a v1450 backup externally per Cryptor's AES-GCM/PBKDF2 format: 68/68 server entries corrupted. Restore completes without any visible error, but everything is empty. A backup created with v1426 (before #1196) contains proper JSON objects and restores fine, even on v1450.

Screenshots

Decrypted v1450 backup (external check):

spis: 68 Eintraege
mbx03741620: PROBLEM -> Typ ist str, kein dict!  ("Spi<user@host:...")
mbx06b21ee9: PROBLEM -> Typ ist str, kein dict!
...

Device

  • OS: Android 16
  • App Version: 1.0.1450 (bug present since 1.0.1448; 1.0.1426 works)

Additional context

Regression introduced by #1196 (18 Jun 2026), which was meant to fix the AppTab enum crash (#1194) but converted a loud failure into silent data corruption for all typed objects.

Suggested fix: in _toEncodable, try (value as dynamic).toJson() before falling back to toString() (or serialize stores explicitly via their models' toJson). Independently, CachedHiveStore._getAndConvert should surface skipped/unparsable entries to the user.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions