Skip to content

Encrypt migration export files with AES-256-GCM#467

Merged
torlando-tech merged 8 commits intotorlando-tech:mainfrom
MatthieuTexier:feature/encrypted-export
Feb 15, 2026
Merged

Encrypt migration export files with AES-256-GCM#467
torlando-tech merged 8 commits intotorlando-tech:mainfrom
MatthieuTexier:feature/encrypted-export

Conversation

@MatthieuTexier
Copy link
Copy Markdown
Contributor

Problem

The .columba export file contains private keys, messages, contacts, and all user data in plaintext. Since this file transits between devices (email, cloud storage, messaging apps…), it is the most exposed attack vector.

Solution

All .columba exports are now encrypted with AES-256-GCM using a user-chosen password, with key derivation via PBKDF2-HMAC-SHA256 (600,000 iterations).

Encrypted file format:

[1 byte:  version 0x02]
[16 bytes: PBKDF2 salt]
[12 bytes: AES-GCM IV/nonce]
[N bytes:  AES-256-GCM ciphertext + 16-byte auth tag]

Changes

  • MigrationCrypto.kt (new): AES-256-GCM encryption/decryption utility with PBKDF2 key derivation, format detection, and custom exceptions
  • MigrationExporter.kt: exportData() now takes a password parameter and encrypts the ZIP after creation
  • MigrationImporter.kt: Auto-detects encrypted vs legacy files (ZIP magic bytes 0x50 0x4B vs version byte 0x02), decrypts when needed, handles wrong password and missing password errors
  • MigrationViewModel.kt: New PasswordRequired and WrongPassword UI states, password flow for export and import
  • MigrationScreen.kt: Password dialog with confirmation for export, single-field prompt for import, validation (min 8 chars, mismatch check), wrong password error display

Backward compatibility

Unencrypted .columba files from older versions import without any password prompt — detection is automatic via the first two bytes of the file.

Tests

92 migration-related tests passing, including:

  • 16 unit tests for MigrationCrypto (round-trip, wrong password, unicode, edge cases)
  • 8 Robolectric tests for MigrationImporter encryption (format detection, encrypted preview, wrong/missing password)
  • 16 Compose UI tests for PasswordDialog (validation, modes, callbacks, integration with screen states)
  • 16 ViewModel tests updated for new signatures
  • All 36 pre-existing migration tests still passing

The .columba export file contains private keys and all user data in
plaintext. Since this file transits between devices (email, cloud, etc.),
it is the most exposed attack vector.

Changes:
- Add MigrationCrypto utility: AES-256-GCM encryption with PBKDF2 key
  derivation (600k iterations, 16-byte salt, 12-byte IV)
- Export now requires a user-chosen password (min 8 chars) and encrypts
  the ZIP before sharing
- Import auto-detects encrypted vs legacy (plaintext ZIP) files using
  magic bytes: 0x02 = encrypted, 0x50 0x4B = legacy ZIP
- Password dialog with confirm field for export, single field for import
- Wrong password detection via GCM auth tag mismatch
- Full backward compatibility: old unencrypted .columba files import
  without password prompt
- 16 new unit tests for MigrationCrypto (round-trip, wrong password,
  format detection, unicode passwords, edge cases)
- All 68 existing migration tests updated and passing
…reen in Codecov

- Add MigrationImporterEncryptionTest (8 tests): isEncryptedExport for
  plaintext/encrypted/empty/invalid files, previewMigration with correct
  password, wrong password, missing password, and plaintext fallback
- Add MigrationScreen.kt to .codecov.yml ignore list (Compose UI screen
  with no testable business logic, like other ignored UI files)
- Total: 76 migration-related tests passing
Replace the .codecov.yml ignore approach with proper test coverage:
- Add MigrationScreenPasswordDialogTest (16 tests): title/description
  display, export/import mode differences, password validation (min
  length, mismatch), confirm/cancel callbacks, show/hide toggle,
  disabled button when empty, wrong password error display
- Add integration tests: Export button triggers password dialog,
  PasswordRequired state shows import dialog, WrongPassword state
  shows error
- Make PasswordDialog internal (was private) for direct testability
- Revert .codecov.yml to original (no ignore changes needed)
- Total: 92 migration-related tests passing
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 15, 2026

Greptile Summary

This PR adds AES-256-GCM encryption to .columba migration export files, protecting private keys, messages, and user data in transit. Key derivation uses PBKDF2-HMAC-SHA256 with 600,000 iterations. Backward compatibility with unencrypted legacy exports is preserved via automatic format detection (ZIP magic bytes vs. version byte 0x02).

  • Crypto layer (MigrationCrypto.kt): Well-structured encryption/decryption with proper key material cleanup, SecureRandom for salt/IV, and clear file format specification
  • Export now requires a password; the ZIP is encrypted in-place after creation
  • Import auto-detects encrypted vs. legacy files, prompts for password when needed, and caches decrypted bytes between preview and import to avoid redundant PBKDF2 derivation
  • UI adds PasswordDialog for both export (with confirmation field) and import (with wrong-password error display) — though the wrong-password error display has a recomposition bug
  • 92 tests cover crypto round-trips, importer encryption paths, UI validation, and updated ViewModel signatures

Confidence Score: 4/5

  • This PR is safe to merge after fixing the wrong-password error display bug in PasswordDialog.
  • The cryptographic implementation is solid (AES-256-GCM, PBKDF2 with 600k iterations, proper key cleanup), backward compatibility is well-handled, and test coverage is thorough with 92 tests. The one issue that lowers confidence is a UI bug where the wrong-password error message won't display due to Compose remember semantics, which degrades the user experience on failed decryption attempts. The style suggestion about min-length validation on import is forward-looking but not a current blocker.
  • app/src/main/java/com/lxmf/messenger/ui/screens/MigrationScreen.kt — the PasswordDialog composable has a recomposition bug that prevents the wrong-password error from being shown to the user.

Important Files Changed

Filename Overview
app/src/main/java/com/lxmf/messenger/migration/MigrationCrypto.kt New AES-256-GCM encryption utility with PBKDF2 key derivation (600k iterations). Crypto primitives are well-chosen, key material is cleared after use, and format detection logic is sound.
app/src/main/java/com/lxmf/messenger/migration/MigrationData.kt Added PreviewWithData class to bundle preview metadata with decrypted ZIP bytes for cache reuse during import.
app/src/main/java/com/lxmf/messenger/migration/MigrationExporter.kt Added required password parameter to exportData() and calls MigrationCrypto.encryptFile() after ZIP creation. Clean integration with minimal changes.
app/src/main/java/com/lxmf/messenger/migration/MigrationImporter.kt Added format detection, encrypted file decryption, decrypted-bytes caching for import reuse, and proper exception propagation for password-related errors. Backward compatibility with plaintext ZIPs is well handled.
app/src/main/java/com/lxmf/messenger/ui/screens/MigrationScreen.kt Added PasswordDialog composable and integrated export/import password flows. Has a bug where wrong-password error message isn't displayed due to remember not re-initializing on recomposition.
app/src/main/java/com/lxmf/messenger/viewmodel/MigrationViewModel.kt Added password flow states (PasswordRequired, WrongPassword), decrypted ZIP caching, and proper exception handling. Password is redacted in toString() of ImportPreview state.

Sequence Diagram

sequenceDiagram
    participant User
    participant Screen as MigrationScreen
    participant VM as MigrationViewModel
    participant Exporter as MigrationExporter
    participant Importer as MigrationImporter
    participant Crypto as MigrationCrypto

    note over User,Crypto: Export Flow
    User->>Screen: Tap "Export All Data"
    Screen->>Screen: Show PasswordDialog (confirm mode)
    User->>Screen: Enter & confirm password
    Screen->>VM: exportData(password)
    VM->>Exporter: exportData(password, onProgress, includeAttachments)
    Exporter->>Exporter: createExportZip(bundle, attachments)
    Exporter->>Crypto: encryptFile(zipFile, password)
    Crypto->>Crypto: PBKDF2 key derivation (600k iterations)
    Crypto->>Crypto: AES-256-GCM encrypt
    Crypto-->>Exporter: Encrypted file
    Exporter-->>VM: Result(Uri)
    VM-->>Screen: ExportComplete state
    Screen->>User: Share sheet

    note over User,Crypto: Import Flow (Encrypted File)
    User->>Screen: Select .columba file
    Screen->>VM: previewImport(uri)
    VM->>Importer: isEncryptedExport(uri)
    Importer->>Crypto: isEncrypted(header)
    Crypto-->>Importer: true
    Importer-->>VM: Result(true)
    VM-->>Screen: PasswordRequired state
    Screen->>Screen: Show PasswordDialog (import mode)
    User->>Screen: Enter password
    Screen->>VM: previewImport(uri, password)
    VM->>Importer: previewMigration(uri, password)
    Importer->>Crypto: decrypt(rawBytes, password)
    Crypto->>Crypto: PBKDF2 + AES-GCM decrypt
    Crypto-->>Importer: Decrypted ZIP bytes
    Importer-->>VM: PreviewWithData (preview + cached zipBytes)
    VM-->>Screen: ImportPreview state
    User->>Screen: Confirm import
    Screen->>VM: importData(uri, password)
    VM->>Importer: importData(uri, password, cachedZipBytes)
    Note over Importer: Reuses cached bytes,<br/>skips redundant decryption
    Importer-->>VM: ImportResult.Success
    VM-->>Screen: ImportComplete state
Loading

Last reviewed commit: bb2a84a

readMigrationBundle now returns the decrypted ZIP bytes alongside the
parsed bundle. importAttachments reuses those bytes directly instead of
re-reading and re-decrypting the file from the URI. This halves peak
memory usage for encrypted imports with attachments.
- ImplicitDefaultLocale: use Locale.ROOT in String.format calls
- ThrowsCount: suppress in decrypt() and readMigrationBundle() where
  multiple distinct error conditions must propagate differently
- SwallowedException: log WrongPasswordException and
  PasswordRequiredException before handling in ViewModel
- NoRelaxedMocks: suppress in MigrationImporterEncryptionTest setUp()
  for infrastructure deps not exercised by encryption tests
CI enforces no new @Suppress("NoRelaxedMocks"). Since the encryption
tests only exercise isEncryptedExport and previewMigration, the
infrastructure deps (database, protocol, etc.) are never called and
strict mocks work without any stubs.
@sentry
Copy link
Copy Markdown
Contributor

sentry bot commented Feb 15, 2026

…pted bytes, redact password

- Call PBEKeySpec.clearPassword() and zero keyBytes after key derivation
  to minimize password/key material lingering in heap memory
- Cache decrypted ZIP bytes from preview for reuse during import, avoiding
  redundant PBKDF2 (600k iterations) + AES-GCM decryption on each import
- Override toString() on ImportPreview to redact password from debug logs
- Add PreviewWithData wrapper to carry decrypted bytes alongside preview
- Update all 40 migration tests for new return types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@torlando-tech
Copy link
Copy Markdown
Owner

@greptileai review

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

… in memory

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@torlando-tech
Copy link
Copy Markdown
Owner

@greptileai

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@torlando-tech torlando-tech merged commit e904921 into torlando-tech:main Feb 15, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants