notif ios: Decrypt E2EE notifications#2230
notif ios: Decrypt E2EE notifications#2230rajveermalviya wants to merge 12 commits intozulip:mainfrom
Conversation
This moves IPHONEOS_DEPLOYMENT_TARGET config to Zulip.xcconfig, instead of it being specified for different Xcode config sections. This vastly simplifies changing the iOS deployment target later, allowing us to change a single variable, instead of navigating through the Xcode UI to change for multiple targets (two currently; Runner and RunnerTests). See: zulip#2156 (comment)
Similar to previous commit, this moves DEVELOPMENT_TEAM config to Zulip.xcconfig, instead of it being specified for different Xcode config sections.
In the Xcode menu bar, Editor -> Structure -> Format file with 'swift-format'.
Carries out the steps 3-4 from "Add iOS app extension" section in the Flutter docs: https://docs.flutter.dev/platform-integration/ios/app-extensions#add-extension In step 3, the Flutter docs walk through adding a "Share Extension" target, we instead add a "Notification Service Extension" target, as described in Apple's documentation: https://developer.apple.com/documentation/usernotifications/modifying-content-in-newly-delivered-notifications#Add-a-service-app-extension-to-your-project The Swift file generated in step 3 was formatted using `swift-format` (In Xcode menu bar, Editor -> Structure -> Format file with 'swift-format'). Also carries out step 5 from the "Open a Flutter app in an iOS app extension" section in the Flutter docs: https://docs.flutter.dev/platform-integration/ios/app-extensions#creating-app-extension-uis-with-flutter this will share the build configurations between the extension target and the Runner target, allowing the extension target to: - Inherit the IPHONEOS_DEPLOYMENT_TARGET config value, from Zulip.xcconfig which {Debug,Release}.xcconfig imports. - Also it pulls in some build config specified by Flutter, including some CocoaPods-related stuff; see for example ios/Flutter/Release.xcconfig. See discussion: zulip#2156 (comment) Also made following other changes to Xcode build settings for the generated NotificationService target: - Deleted "iOS Deployment Target" config specified for target, so that when building the target, the inherited value from Zulip.xcconfig will be used. By navigating to project navigator (View > Navigators > Project) > NotificationService under TARGETS > Build Settings (tab) > Deployment, then pressed the Delete key on keyboard after selecting "iOS Deployment Target". - Updated the versioning information to match the Runner target. By navigating to project navigator (View > Navigators > Project) > NotificationService under TARGETS > Build Settings (tab) > Versioning, and changing the following values: * Changed "Current Project Version" to "$(FLUTTER_BUILD_NUMBER)" (in Xcode text field, typed without the double-quotes), where the generated value was "1". * Changed "Marketing Version" to "$(FLUTTER_BUILD_NAME)" (same as previous one, no double-quotes), where the default value was "1.0". * Changed "Versioning System" to "Apple Generic", where the default was set to "None". See discussion: zulip#2156 (comment)
0fecb87 to
2f94b2f
Compare
…xtension Implement initial setup of a headless FlutterEngine for running Dart code in the NotificationService extension. This involves some Swift code to start the engine and drive its event loop, and a trivial Dart function to serve as the entry point. It also involves some build setup. For the build setup, this carries out the steps 2-3 from the "Register plugins" section in Flutter's guide to iOS app extensions: https://docs.flutter.dev/platform-integration/ios/app-extensions#register-plugins and adds the following pre-action script for the Runner scheme: /bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare by following same steps listed under step 4 in "Open a Flutter app in an iOS app extension" section in Flutter's guide to iOS app extensions: https://docs.flutter.dev/platform-integration/ios/app-extensions#creating-app-extension-uis-with-flutter but for the Runner scheme instead of the extension scheme. And also selected the Runner target instead of the extension target for the "Provide build settings" drop-down list. That script copies the Flutter framework to a place where Xcode will find it when building the NotificationService target. See discussion: zulip#2156 (comment) We are skipping the step 3 of "Open a Flutter app in an iOS app extension": https://docs.flutter.dev/platform-integration/ios/app-extensions#creating-app-extension-uis-with-flutter because disabling user script sandboxing is only needed if we had added a "Run Script" build phase without specifying input and output files. It seems it is unnecessary for the pre-action script because Xcode UI for adding the script doesn't have the options for specifying input and output files. See discussion: zulip#2156 (comment)
Fixes zulip#1265. This uses Pigeons's handy `@FlutterApi` API to define an interface method that allows passing arguments from Swift to Dart and also get a return value back from Dart to Swift.
…roup This is required to share any files like database between both app (Runner) and the service extension targets.
This will allow the Dart code executing in the notification service extension to access the same database file as the main app target.
This is needed for (soon to be added) NotificationService target to initialize the Pigeon API.
…less Flutter engine
These types will soon be used to parse encrypted data from iOS APNs payloads. So, rename them to be neutral about the push notification service they come from.
869495a to
9616f4a
Compare
9616f4a to
b5abc3b
Compare
|
Thanks! I'm not yet convinced about the approach in the rename commit: I started a CZO discussion about it: #mobile-team > E2EE: parsing code @ 💬#mobile-team > E2EE: parsing code @ 💬 |
| Future<ImprovedNotificationContent> _didReceivePushNotification(NotificationContent notifContent) async { | ||
| final parsed = EncryptedApnsPayload.fromJson(notifContent.payload.cast()); |
There was a problem hiding this comment.
What happens when this receives a legacy notification? Does it just throw?
That might have the right behavior already, because IIUC we don't actually need to do anything with the legacy notification. But we should handle it explicitly; it shouldn't be treated as an error.
gnprice
left a comment
There was a problem hiding this comment.
Thanks @rajveermalviya for building this!
Comments below. See also the chat thread for Chris's comment above.
One high-level observation, which several of these comments add detail to: the main commit:
b5abc3b notif ios: Decrypt E2EE notifications
is doing too many things at once. It should be broken up into several separate commits, in order to make each of its changes more readable by disentangling them from the others.
| B34E9F092D776BEB0009AED2 /* Notifications.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34E9F082D776BEB0009AED2 /* Notifications.g.swift */; }; | ||
| B35E11A62F484E6800DE4085 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; | ||
| B378A5012F45B08F0031EFA1 /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B378A4FA2F45B08F0031EFA1 /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; | ||
| B3D425322F6D40C200F9AE69 /* IosNative.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B340EB372F5B092B007AD309 /* IosNative.g.swift */; }; |
There was a problem hiding this comment.
Hmm, this line is added in this commit:
0270431 notif: Setup IosNativeHostApi Pigeon API for NotificationService headless Flutter engine
But that file isn't new (or even touched) in that commit; in fact it's already present in main, added originally in 1d03e57 (#2195).
What caused this line to appear in this particular commit?
Should this line have been present all along? What's been the effect of it not being present?
| B35E11A62F484E6800DE4085 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; | ||
| B378A5012F45B08F0031EFA1 /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B378A4FA2F45B08F0031EFA1 /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; | ||
| B3D425322F6D40C200F9AE69 /* IosNative.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = B340EB372F5B092B007AD309 /* IosNative.g.swift */; }; | ||
| B3D425332F6D40C200F9AE69 /* IosNativeHostApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32717682F6C49E5007682B1 /* IosNativeHostApi.swift */; }; |
There was a problem hiding this comment.
This line is added in this commit (same as the comment above):
0270431 notif: Setup IosNativeHostApi Pigeon API for NotificationService headless Flutter engine
But that file isn't new in that commit, or touched there; it was introduced in the previous commit:
9882e18 ios [nfc]: Move IosNativeHostApiImpl to its own Swift file
Should this line have been added in that commit? What caused it to appear in this file?
It also looks quite a lot like another line a few lines above:
B32717692F6C49E5007682B1 /* IosNativeHostApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = B32717682F6C49E5007682B1 /* IosNativeHostApi.swift */; };
which does appear in that earlier commit. What's the effect of having both lines, instead of just the line that was added later? Should we have only one of them?
| /// Parsed version of an FCM message, of any plaintext type. | ||
| /// An APNs payload whose contents are encrypted end-to-end from the Zulip server. | ||
| /// | ||
| /// Once decrypted, the contents will become an [NotifMessage]. |
There was a problem hiding this comment.
nit:
| /// Once decrypted, the contents will become an [NotifMessage]. | |
| /// Once decrypted, the contents will become a [NotifMessage]. |
(similarly above. "Notif" starts with a consonant sound, while "Fcm" starts with a vowel sound: "eff cee em".)
| final String token; | ||
| final int timestamp; | ||
| // final String? iosAppId; // TODO(#1764) | ||
| final String? iosAppId; |
There was a problem hiding this comment.
This can be easily split out into a prep commit.
| static String titleForNotifMessage(MessageNotifMessage data, ZulipLocalizations zulipLocalizations) { | ||
| return switch (data.recipient) { | ||
| NotifMessageChannelRecipient(:var channelName?, :var topic) => | ||
| '#$channelName > ${topic.displayName}', | ||
| NotifMessageChannelRecipient(:var topic) => | ||
| '#${zulipLocalizations.unknownChannelName} > ${topic.displayName}', // TODO get stream name from data | ||
| NotifMessageDmRecipient(:var allRecipientIds) when allRecipientIds.length > 2 => | ||
| zulipLocalizations.notifGroupDmConversationLabel( | ||
| data.senderFullName, allRecipientIds.length - 2), // TODO use others' names, from data | ||
| NotifMessageDmRecipient() => | ||
| data.senderFullName, | ||
| }; | ||
| } | ||
|
|
||
| static Uri notificationUrlForNotifMessage(MessageNotifMessage data) { |
There was a problem hiding this comment.
This factoring-out can happen as its own prep commit.
| Future<void> checkNotification( | ||
| FakeAsync async, |
There was a problem hiding this comment.
This parameter doesn't seem to get used.
The use of FakeAsync is a significant complication to thinking about the tests. So if it's not getting used, best to leave it out.
| payload = { | ||
| // Only `notification_url` value is read when notification is opened. | ||
| ...fakeEncryptedApnsPayload(), | ||
| 'notification_url': notificationUrl.toString(), |
There was a problem hiding this comment.
This seems like a further sign that the original payload doesn't belong in the data used for opening the notification 🙂
| final Map<Object?, Object?> payload; | ||
| if (encrypted) { | ||
| final notificationUrl = notificationUrlForMessage(account, message).toString(); | ||
| payload = { | ||
| // Only `notification_url` value is read when notification is opened. | ||
| ...fakeEncryptedApnsPayload(), | ||
| 'notification_url': notificationUrl, | ||
| }; | ||
| } else { | ||
| payload = messageLegacyApnsPayload(message, account: account); | ||
| } | ||
|
|
||
| // Set up a value to return for | ||
| // `notificationPigeonApi.getNotificationDataFromLaunch`. | ||
| final payload = messageApnsPayload(message, account: account); |
There was a problem hiding this comment.
This logic should all get moved into a helper next to messageLegacyApnsPayload — say, called messageApnsPayload. That way (a) we don't have two copies of it, here and just above; (b) those details are out of the way when reading these two helpers.
| await checkOpenNotification( | ||
| tester, | ||
| eg.selfAccount, | ||
| eg.streamMessage(), | ||
| encrypted: false); |
There was a problem hiding this comment.
nit:
| await checkOpenNotification( | |
| tester, | |
| eg.selfAccount, | |
| eg.streamMessage(), | |
| encrypted: false); | |
| await checkOpenNotification(tester, eg.selfAccount, eg.streamMessage(), | |
| encrypted: false); |
That's both more compact, and more parallel to the test case above — makes it easier to compare them by eye and spot the one difference.
| await checkOpenNotification(tester, eg.selfAccount, eg.streamMessage()); | ||
| }, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS})); | ||
|
|
||
| testWidgets('iOS: stream message, legacy plaintext', (tester) async { |
There was a problem hiding this comment.
nit: "iOS legacy plaintext" is one concept here (at least, "legacy plaintext" is only meaningful within the context of "iOS", since it's about the way we were encoding things on iOS notifications specifically), so put it in one place in the description:
| testWidgets('iOS: stream message, legacy plaintext', (tester) async { | |
| testWidgets('stream message: iOS legacy plaintext', (tester) async { |
Stacked on top of #2156.