Skip to content

Comments

feat: support EAR with incremental sync - WPB-17302#4310

Open
David-Henner wants to merge 43 commits intofeat/ear-support-without-new-syncfrom
feat/ear-with-new-sync
Open

feat: support EAR with incremental sync - WPB-17302#4310
David-Henner wants to merge 43 commits intofeat/ear-support-without-new-syncfrom
feat/ear-with-new-sync

Conversation

@David-Henner
Copy link
Contributor

@David-Henner David-Henner commented Feb 13, 2026

Issue

This PR add support for EAR with the incremental sync. This means we encrypt the update events when saving them in the events database, and decrypt them when fetching them for processing.

Most of the events can be accessible only when the app is unlocked. However, calling events need to be processed while in the background, so their access level is less restrictive and can be encrypted / decrypted without the app being unlocked. This is why we have primary and secondary encryption keys.

Changes

IncrementalSync

  • Added earService and notificationContext dependencies
  • New: waitForDatabaseUnlock() method with 3-second timeout
  • Key Flow Changes:
    • If we're in foreground and database is locked, abort sync
    • Fetch public / private keys before syncing
    • Pass public keys when pulling events
    • Pass private keys when processing stored events
    • Pass backgroundAccessibleOnly to live & stored event processing

UpdateEventsLocalStore

  • Encryption on Write (persistEventEnvelope):
    • Accepts optional publicKeys parameter
    • Encrypts event data with primary or secondary key based on event type
    • Sets isEncrypted and isBackgroundAccessible properties
  • Decryption on Read (fetchStoredEventEnvelopes):
    • Accepts optional privateKeys and backgroundAccessibleOnly parameters
    • Filters events based on background accessibility (for processing of calling events in NSE)
    • Decrypts encrypted events with appropriate private key - primary vs secondary

Core Data

StoredUpdateEventEnvelope entity now includes:

  • isEncrypted: indicates if event data is encrypted
  • isBackgroundAccessible: indicates if the event should be accessible in the background (for calling events)

New Files Added

  1. EAREncryptionHelper.swift - Encryption/decryption utilities for event data
  2. ProtobufMessageDecoder.swift - Helper for decoding protobuf messages
  3. UpdateEventEnvelope+BackgroundAccessible.swift - Extension determining if events are calling-related
  4. DatabaseEncryptionLockNotification.swift - Notification type for database lock/unlock events (moved from sync-engine)

Testing

  • Log in
  • Go to Settings > Account > Encrypt messages at rest - enable
  • Background the app
  • Receive messages or other events
  • Open the app
  • Verify the messages / events are processed

Checklist

  • Title contains a reference JIRA issue number like [WPB-XXX].
  • Description is filled and free of optional paragraphs.
  • Adds/updates automated tests.

@David-Henner David-Henner changed the title feat: ear with new sync - WIP feat: support EAR with incremental sync - WPB-17302 Feb 17, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Feb 17, 2026

Test Results

2 tests   2 ✅  0s ⏱️
2 suites  0 💤
1 files    0 ❌

Results for commit e0296f0.

♻️ This comment has been updated with latest results.

Summary: workflow run #22235654623
Allure report (download zip): html-report-28050-feat_ear-with-new-sync

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds comprehensive support for Encryption at Rest (EAR) with incremental synchronization. The implementation encrypts update events when storing them in the events database and decrypts them when fetching for processing. The key innovation is using dual encryption keys (primary and secondary) to enable background processing of calling events while maintaining security for other event types.

Changes:

  • Core Data schema upgraded to v7.0 with isEncrypted and isBackgroundAccessible flags for event envelopes
  • IncrementalSync enhanced with database unlock waiting mechanism and encryption key management
  • UpdateEventsLocalStore modified to encrypt events on write and decrypt on read using appropriate keys
  • New helper files for encryption operations and protobuf message decoding
  • SelfPostingNotification protocol and DatabaseEncryptionLockNotification moved to WireDataModel for shared access

Reviewed changes

Copilot reviewed 39 out of 41 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
IncrementalSync.swift Added database unlock waiting logic, key fetching, and passing encryption keys through sync pipeline
UpdateEventsLocalStore.swift Implemented encryption on persist and decryption on fetch with primary/secondary key selection
UpdateEventEnvelope+BackgroundAccessible.swift Logic to determine if events contain calling content for secondary key usage
EAREncryptionHelper.swift Wrapper functions for SecKey encryption/decryption operations
ProtobufMessageDecoder.swift Helper to decode and validate protobuf messages for event type detection
StoredUpdateEventEnvelope.swift Added isEncrypted and isBackgroundAccessible Core Data attributes
ZMEventModel7.0 New Core Data schema version with encryption-related attributes
UpdateEventMigrator.swift Updated to fetch and use private keys during legacy event migration
NSEClientScope.swift Modified to fetch public keys for encrypting events pulled in NSE
Multiple test files Updated mocks and added comprehensive tests for encryption/decryption scenarios

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Collaborator

@netbe netbe left a comment

Choose a reason for hiding this comment

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

left some comments, the main point I am a bit fuzzy about is when DatabaseEncryptionLockNotification is posted, and if we could use publisher instead of notification

accountID: accountID,
coreDataStack: coreDataStack,
sharedUserDefaults: dependency.sharedUserDefaults,
authenticationContext: AuthenticationContext(storage: LAContextStorage())
Copy link
Collaborator

Choose a reason for hiding this comment

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

question: isn't the LAContextStorage() requiring a biometric prompt?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's LAContext() that does IIRC

enum Error: Swift.Error {
case failedToFetchStoredEvents(Swift.Error)
case failedToDeleteStoredEvents(Swift.Error)
case failedToEncryptEventData
Copy link
Collaborator

Choose a reason for hiding this comment

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

suggestion: add associated error?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

data: data,
privateKey: key
) else {
WireLogger.ear.error("failed to decrypt stored event", attributes: .safePublic)
Copy link
Collaborator

Choose a reason for hiding this comment

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

question: can we log the eventId as envelope_event_id attribute?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

not available on the stored event

storedEventEnvelope.isEncrypted = true
storedEventEnvelope.isBackgroundAccessible = isBackgroundAccessible
} else {
WireLogger.ear.error("failed to encrypt event", attributes: .safePublic)
Copy link
Collaborator

Choose a reason for hiding this comment

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

suggestion: add event id in attributes if possible

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added 2ea7235

"timed out waiting for database unlock, cancelling sync",
attributes: .incrementalSyncV2, .safePublic
)
throw Failure.databaseUnlockTimeout
Copy link
Collaborator

Choose a reason for hiding this comment

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

question: is the failure propagated?
does it mean that if it timesout? the incremental sync is cancelled?
why would it timeout?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

outdated b04d292

@datadog-wireapp
Copy link

datadog-wireapp bot commented Feb 18, 2026

✅ Tests

🎉 All green!

❄️ No new flaky tests detected
🧪 All tests passed

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: e0296f0 | Docs | Was this helpful? Give us feedback!

…ew-sync

# Conflicts:
#	wire-ios-data-model/Source/Authentication/EAR/EARService.swift
import Foundation
import GenericMessageProtocol

struct ProtobufMessageDecoder {
Copy link
Collaborator

Choose a reason for hiding this comment

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

question: it looks like this was restored to its original implementation, but changes were made recently to throw errors. I think we should preserve these changes.

"failed to decrypt stored event: no private keys",
attributes: .safePublic
)
return nil
Copy link
Collaborator

Choose a reason for hiding this comment

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

issue: returning nil here means we just skip over this event and will proceed to process the rest. This could lead to some unexpected issues given the assumption that events are processed in order. What do you think about throwing an error and aborting? Perhaps this comes with other issues. what do you think @netbe ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added critical logs and threw errors 6a2e408

}

func perform(appState: UIApplication.State) async throws -> Token {
let inBackground = appState == .background
Copy link
Collaborator

Choose a reason for hiding this comment

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

question: why do we need to check if we're in the BG?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

made an attempt to update the flow as discussed 65fd560

publicKeys: EARPublicKeys?,
backgroundAccessibleOnly: Bool
) async {
guard !backgroundAccessibleOnly || (backgroundAccessibleOnly && envelope.isBackgroundAccessible) else {
Copy link
Collaborator

Choose a reason for hiding this comment

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

question: why do we abort if we get a live event that is not background accessible? To avoid processing it before we processed earlier stored events?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added comment 927a65c

Comment on lines 442 to 443
let isLocked = earService.isLocked
let privateKeys = try earService.fetchPrivateKeys(includingPrimary: !isLocked)
Copy link
Collaborator

Choose a reason for hiding this comment

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

question: so we will attempt to migrate event if the database is locked? I would expect that a prerequisite would be to have an unlocked database.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed here c192c71

@netbe netbe added the release label Feb 20, 2026
@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants