From 39df8c455db4013d387c2f8f9829be6be3d52206 Mon Sep 17 00:00:00 2001 From: Matthieu Finiasz Date: Mon, 31 Jul 2023 21:41:38 +0200 Subject: [PATCH] v0.15.0 (build 207) --- CHANGELOG.md | 12 +- .../io/olvid/engine/backup/BackupManager.java | 26 +- .../olvid/engine/channel/ChannelManager.java | 81 +- .../channel/databases/ObliviousChannel.java | 47 +- .../channel/datatypes/NetworkChannel.java | 5 +- .../io/olvid/engine/datatypes/Constants.java | 7 +- .../ExponentialBackoffRepeatingScheduler.java | 2 +- .../NoAcceptableChannelException.java} | 6 +- .../PushNotificationTypeAndParameters.java | 63 +- .../olvid/engine/datatypes/ServerMethod.java | 1 + .../io/olvid/engine/datatypes/Session.java | 13 +- .../datatypes/containers/DialogType.java | 36 +- .../engine/datatypes/containers/GroupV2.java | 18 +- .../datatypes/containers/ServerQuery.java | 154 +- .../notifications/DownloadNotifications.java | 7 +- .../notifications/IdentityNotifications.java | 17 +- .../notifications/ProtocolNotifications.java | 16 + .../java/io/olvid/engine/engine/Engine.java | 448 ++- ...ificationListenerChannelsAndProtocols.java | 66 +- .../engine/NotificationListenerDownloads.java | 31 + .../engine/NotificationListenerGroups.java | 4 +- .../engine/NotificationListenerGroupsV2.java | 11 +- .../engine/NotificationListenerIdentity.java | 13 + .../olvid/engine/engine/types/EngineAPI.java | 31 +- .../engine/types/EngineNotifications.java | 29 +- .../engine/engine/types/JsonGroupType.java | 116 + .../engine/engine/types/ObvBytesKey.java | 45 +- .../engine/engine/types/ObvDeviceList.java | 34 + .../types/ObvDeviceManagementRequest.java | 123 + .../olvid/engine/engine/types/ObvDialog.java | 53 +- .../engine/types/ObvGroupOwnerAndUidKey.java | 87 + .../engine/types/ObvPushNotificationType.java | 53 + .../engine/types/RegisterApiKeyResult.java | 26 + .../SimpleEngineNotificationListener.java | 56 + .../engine/types/identities/ObvGroupV2.java | 11 +- .../engine/types/identities/ObvIdentity.java | 4 + .../types/identities/ObvKeycloakState.java | 18 +- .../types/identities/ObvOwnedDevice.java | 62 + .../types/sync/ObvBackupAndSyncDelegate.java | 41 + .../engine/engine/types/sync/ObvSyncAtom.java | 475 +++ .../engine/engine/types/sync/ObvSyncDiff.java | 53 + .../engine/types/sync/ObvSyncSnapshot.java | 107 + .../types/sync/ObvSyncSnapshotNode.java | 31 + .../engine/identity/IdentityManager.java | 398 ++- .../identity/databases/ContactDevice.java | 5 +- .../identity/databases/ContactGroup.java | 25 +- .../identity/databases/ContactGroupV2.java | 62 +- .../identity/databases/ContactIdentity.java | 50 +- .../identity/databases/KeycloakServer.java | 52 +- .../identity/databases/OwnedDevice.java | 153 +- .../identity/databases/OwnedIdentity.java | 173 +- .../databases/OwnedIdentityDetails.java | 18 +- .../databases/sync/ContactSyncSnapshot.java | 48 + .../databases/sync/GroupV1SyncSnapshot.java | 50 + .../databases/sync/GroupV2SyncSnapshot.java | 50 + .../sync/IdentityDetailsSyncSnapshot.java | 116 + .../sync/IdentityManagerSyncSnapshot.java | 112 + .../databases/sync/KeycloakSyncSnapshot.java | 116 + .../sync/OwnedIdentitySyncSnapshot.java | 115 + .../engine/metamanager/BackupDelegate.java | 5 +- .../engine/metamanager/ChannelDelegate.java | 3 +- .../EncryptionForIdentityDelegate.java | 1 + .../EngineOwnedIdentityCleanupDelegate.java | 29 + .../engine/metamanager/IdentityDelegate.java | 42 +- .../metamanager/NetworkFetchDelegate.java | 4 +- .../metamanager/PushNotificationDelegate.java | 3 +- .../metamanager/SolveChallengeDelegate.java | 3 - .../engine/networkfetch/FetchManager.java | 62 +- ...terServerPushNotificationsCoordinator.java | 27 +- .../coordinators/ServerQueryCoordinator.java | 20 +- .../coordinators/WebsocketCoordinator.java | 51 +- .../PushNotificationConfiguration.java | 58 +- .../networkfetch/databases/ServerSession.java | 4 + ...CreateServerSessionCompositeOperation.java | 2 +- .../operations/FreeTrialOperation.java | 1 - .../RegisterPushNotificationOperation.java | 78 +- .../operations/RequestChallengeOperation.java | 19 +- .../operations/ServerQueryOperation.java | 254 +- .../StandaloneServerQueryOperation.java | 105 + .../operations/VerifyReceiptOperation.java | 63 +- .../engine/protocol/ProtocolManager.java | 249 +- .../coordinators/ProtocolStepCoordinator.java | 14 +- .../ChannelCreationProtocolInstance.java | 3 +- .../protocol/databases/ProtocolInstance.java | 63 +- .../datatypes/ProtocolManagerSession.java | 13 +- .../datatypes/ProtocolStarterDelegate.java | 14 +- .../protocol_engine/ConcreteProtocol.java | 29 +- .../protocol_engine/ProtocolOperation.java | 4 +- .../protocol_engine/ProtocolStep.java | 4 +- ...nnelCreationWithContactDeviceProtocol.java | 40 +- ...hannelCreationWithOwnedDeviceProtocol.java | 1250 +++++++ .../protocols/ContactManagementProtocol.java | 89 +- .../ContactMutualIntroductionProtocol.java | 171 +- .../DeviceCapabilitiesDiscoveryProtocol.java | 9 +- .../protocols/DeviceDiscoveryProtocol.java | 13 +- .../protocols/GroupInvitationProtocol.java | 23 +- .../protocols/GroupManagementProtocol.java | 318 +- .../protocol/protocols/GroupsV2Protocol.java | 321 +- .../IdentityDetailsPublicationProtocol.java | 126 +- .../KeycloakBindingAndUnbindingProtocol.java | 174 +- .../KeycloakContactAdditionProtocol.java | 15 +- .../OneToOneContactInvitationProtocol.java | 25 +- .../OwnedDeviceDiscoveryProtocol.java | 376 +++ .../OwnedDeviceManagementProtocol.java | 328 ++ .../OwnedIdentityDeletionProtocol.java | 767 +++++ ...letionWithContactNotificationProtocol.java | 521 --- .../protocols/SynchronizationProtocol.java | 761 +++++ ...stEstablishmentWithMutualScanProtocol.java | 27 +- .../TrustEstablishmentWithSasProtocol.java | 36 +- .../inspectionProfiles/Project_Default.xml | 3 + obv_messenger/app/build.gradle | 18 +- .../64.json | 2896 +++++++++++++++++ .../olvid/messenger/billing/BillingUtils.java | 297 +- .../billing/SubscriptionPurchaseFragment.java | 119 +- .../discussion/location/LatLngWrapper.java | 4 +- .../firebase/ObvFirebaseMessagingService.java | 24 +- .../app/src/main/AndroidManifest.xml | 7 +- .../src/main/java/io/olvid/messenger/App.java | 41 +- .../messenger/AppBackupAndSyncDelegate.java | 51 + .../java/io/olvid/messenger/AppSingleton.java | 114 +- .../EngineNotificationProcessor.java | 129 +- ...ngineNotificationProcessorForContacts.java | 89 +- .../EngineNotificationProcessorForGroups.java | 45 +- ...ngineNotificationProcessorForGroupsV2.java | 190 -- .../EngineNotificationProcessorForGroupsV2.kt | 229 ++ .../activities/ContactDetailsActivity.java | 51 +- .../activities/GroupCreationActivity.java | 618 ---- .../activities/GroupDetailsActivity.java | 1024 ------ .../activities/GroupV2DetailsActivity.java | 1254 ------- .../activities/MessageDetailsActivity.java | 45 +- .../OwnedIdentityDetailsActivity.java | 447 ++- .../messenger/activities/ShareActivity.java | 2 +- .../appdialogs/AppDialogShowActivity.java | 24 +- .../IdentityDeactivatedDialogFragment.java | 316 +- .../ConfigurationSettingsPojo.java | 4 +- .../olvid/messenger/customClasses/Markdown.kt | 1 - .../MessageAttachmentAdapter.java | 2 +- .../customClasses/MuteNotificationDialog.java | 2 +- .../customClasses/SelectableArrayAdapter.java | 2 +- .../messenger/customClasses/StringUtils.java | 26 +- .../messenger/customClasses/StringUtils2.kt | 2 +- .../messenger/databases/AppDatabase.java | 6 +- .../databases/AppDatabaseMigrations.java | 8 + .../databases/AppDatabaseOpenCallback.java | 19 +- .../databases/dao/ContactGroupJoinDao.java | 4 +- .../databases/dao/DiscussionDao.java | 28 +- .../databases/dao/Group2MemberDao.java | 20 + .../messenger/databases/dao/GroupDao.java | 32 +- .../messenger/databases/dao/MessageDao.java | 24 +- .../databases/dao/OwnedDeviceDao.java | 80 + .../databases/dao/OwnedIdentityDao.java | 2 +- .../databases/dao/PendingGroupMemberDao.java | 2 +- .../databases/entity/CallLogItem.java | 4 + .../messenger/databases/entity/Contact.java | 3 + .../databases/entity/Discussion.java | 87 +- .../entity/DiscussionCustomization.java | 76 +- .../entity/FyleMessageJoinWithStatus.java | 9 +- .../messenger/databases/entity/Message.java | 1441 +------- .../entity/MessageRecipientInfo.java | 2 +- .../databases/entity/OwnedDevice.java | 101 + .../databases/entity/OwnedIdentity.java | 16 +- .../entity/RemoteDeleteAndEditRequest.java | 7 +- .../entity/jsons/JsonDeleteDiscussion.java | 104 + .../entity/jsons/JsonDeleteMessages.java | 128 + .../entity/jsons/JsonDiscussionRead.java | 117 + .../entity/jsons/JsonExpiration.java | 109 + .../JsonLimitedVisibilityMessageOpened.java | 116 + .../databases/entity/jsons/JsonLocation.java | 289 ++ .../databases/entity/jsons/JsonMessage.java | 214 ++ .../entity/jsons/JsonMessageReference.java | 72 + .../jsons/JsonOneToOneMessageIdentifier.java | 55 + .../databases/entity/jsons/JsonPayload.java | 167 + .../entity/jsons/JsonQuerySharedSettings.java | 132 + .../databases/entity/jsons/JsonReaction.java | 133 + .../entity/jsons/JsonReturnReceipt.java | 53 + .../jsons/JsonScreenCaptureDetection.java | 103 + .../entity/jsons/JsonSharedSettings.java | 105 + .../entity/jsons/JsonUpdateMessage.java | 165 + .../entity/jsons/JsonUserMention.java | 95 + .../entity/jsons/JsonWebrtcMessage.java | 65 + .../entity/sync/AppSyncSnapshot.java | 127 + .../entity/sync/ContactSyncSnapshot.java | 117 + .../DiscussionCustomizationSyncSnapshot.java | 74 + .../entity/sync/GroupV1SyncSnapshot.java | 108 + .../entity/sync/GroupV2SyncSnapshot.java | 108 + .../sync/OwnedIdentitySyncSnapshot.java | 202 ++ .../entity/sync/SettingsSyncSnapshot.java | 123 + .../tasks/AddFyleToDraftFromUriTask.java | 6 + .../databases/tasks/ApplySyncAtomTask.kt | 170 + .../tasks/CreateOrUpdateGroupV2Task.java | 114 +- .../databases/tasks/DeleteMessagesTask.java | 6 +- .../tasks/ExpiringOutboundMessageSent.java | 5 +- .../databases/tasks/ForwardMessagesTask.java | 13 +- .../databases/tasks/GroupCloningTasks.java | 11 +- .../HandleNewMessageNotificationTask.java | 511 ++- .../tasks/HandleReceiveReturnReceipt.java | 4 +- .../tasks/InboundEphemeralMessageClicked.java | 46 +- .../InsertMediatorInvitationMessageTask.kt | 53 +- ...nedDevicesSynchronisationWithEngineTask.kt | 77 + .../PostLocationMessageInDiscussionTask.java | 13 +- .../tasks/PostMessageInDiscussionTask.java | 20 +- ...ostOsmLocationMessageInDiscussionTask.java | 13 +- .../PropagatePinnedDiscussionsChangeTask.kt | 49 + .../tasks/ReplaceDiscussionDraftTask.java | 3 +- .../databases/tasks/SaveDraftTask.java | 14 +- .../tasks/SetDraftJsonExpirationTask.java | 5 +- .../databases/tasks/SetDraftReplyTask.java | 3 +- .../tasks/UpdateAllGroupMembersNames.java | 4 +- ...eContactCustomDisplayNameAndPhotoTask.java | 32 +- .../UpdateGroupCustomNameAndPhotoTask.java | 25 +- .../tasks/UpdateGroupNameAndPhotoTask.java | 5 + .../UpdateGroupV2CustomNameAndPhotoTask.java | 23 +- .../tasks/UpdateLocationMessageTask.java | 7 +- .../databases/tasks/UpdateMessageBodyTask.kt | 3 +- .../databases/tasks/UpdateReactionsTask.java | 14 +- .../tasks/backup/AppBackupPojos.java | 14 +- .../backup/RestoreAppDataFromBackupTask.java | 16 +- .../tasks/backup/SettingsPojo_0.java | 28 +- .../discussion/ComposeMessageViewModel.kt | 23 +- .../discussion/DiscussionActivity.java | 213 +- .../discussion/DiscussionViewModel.java | 6 +- .../ForwardMessagesDialogFragment.java | 2 +- .../discussion/MessageLongPressPopUp.java | 14 +- ...essageEphemeralSettingsDialogFragment.java | 21 +- .../io/olvid/messenger/discussion/Utils.java | 17 +- .../discussion/VoiceMessageRecorder.java | 5 +- .../linkpreview/LinkPreviewRepository.kt | 12 +- .../discussion/linkpreview/OpenGraphParser.kt | 2 + .../linkpreview/UserAgentInterceptor.kt | 5 +- .../FullscreenMapBottomSheetDialog.java | 3 +- .../location/FullscreenMapDialogFragment.java | 7 +- ...apLibreCustomAttributionDialogManager.java | 15 +- .../SendLocationBasicDialogFragment.java | 4 +- .../discussion/mention/MentionViewModel.kt | 20 +- .../settings/DiscussionSettingsDataStore.java | 53 +- .../DiscussionSettingsHeadersFragment.java | 3 +- .../settings/DiscussionSettingsViewModel.java | 3 +- .../FilteredContactListFragment.java | 5 +- .../fragments/SubscriptionStatusFragment.java | 39 +- .../ContactIntroductionDialogFragment.kt | 25 +- .../EditNameAndPhotoDialogFragment.java | 6 +- .../GroupV2MemberAdditionDialogFragment.java | 175 - .../messenger/gallery/GalleryActivity.java | 3 +- .../EditOwnedGroupDetailsDialogFragment.kt | 285 ++ .../messenger/group/GroupCreationActivity.kt | 674 ++++ .../messenger/group/GroupCreationViewModel.kt | 133 + .../GroupCustomSettingsPreferenceFragment.kt | 236 ++ .../messenger/group/GroupDetailsActivity.kt | 1105 +++++++ .../messenger/group/GroupDetailsViewModel.kt | 47 + .../olvid/messenger/group/GroupTypeModel.kt | 126 + .../messenger/group/GroupV2DetailsActivity.kt | 1393 ++++++++ .../group/GroupV2DetailsViewModel.kt | 537 +++ .../GroupV2MemberAdditionDialogFragment.kt | 168 + .../group/OwnedGroupDetailsFragment.kt | 386 +++ .../group/OwnedGroupDetailsViewModel.kt | 176 + .../components/GroupAdminsSelectionDialog.kt | 171 + .../group/components/GroupTypeSelection.kt | 193 ++ .../io/olvid/messenger/main/MainActivity.kt | 8 +- .../messenger/main/calls/CallLogViewModel.kt | 10 +- .../main/contacts/ContactListItem.kt | 66 +- .../main/contacts/ContactListScreen.kt | 2 +- .../main/contacts/ContactListViewModel.kt | 9 +- .../discussions/DiscussionListFragment.kt | 14 +- .../main/discussions/DiscussionListScreen.kt | 2 +- .../discussions/DiscussionListViewModel.kt | 18 +- .../main/groups/GroupListFragment.kt | 4 +- .../main/invitations/InvitationListItem.kt | 7 +- .../invitations/InvitationListViewModel.kt | 44 +- .../AndroidNotificationManager.java | 9 +- .../NotificationActionService.java | 22 +- .../onboarding/IdentityCreationFragment.java | 3 +- .../messenger/openid/KeycloakManager.java | 23 +- .../EditOwnedGroupDetailsDialogFragment.java | 194 -- ...ditOwnedIdentityDetailsDialogFragment.java | 8 + .../OwnedGroupDetailsFragment.java | 281 -- .../OwnedGroupDetailsViewModel.java | 195 -- .../SelectDetailsPhotoActivity.java | 2 + .../ConfigurationScannedFragment.java | 34 +- .../plus_button/KeycloakBindFragment.java | 3 +- .../services/NetworkStateMonitorReceiver.java | 7 +- .../services/UnifiedForegroundService.java | 10 +- .../ContactsAndGroupsPreferenceFragment.java | 21 + .../settings/OtherPreferenceFragment.java | 4 +- ...SendReceiveMessagesPreferenceFragment.java | 18 + .../messenger/settings/SettingsActivity.java | 29 +- .../viewModels/GroupCreationViewModel.java | 115 - .../viewModels/GroupDetailsViewModel.java | 60 - .../viewModels/GroupV2DetailsViewModel.java | 411 --- .../webclient/ColissimoMessageQueue.java | 9 +- .../listeners/DiscussionListener.java | 5 +- .../webclient/listeners/MessageListener.java | 7 +- .../messenger/webrtc/WebrtcCallService.java | 131 +- .../ActionShortcutConfigurationViewModel.java | 2 +- .../ActionShortcutSendMessageActivity.java | 8 +- .../app/src/main/res/drawable/ic_device.xml | 5 + .../res/drawable/ic_device_expiration.xml | 5 + .../main/res/drawable/ic_device_inactive.xml | 5 + ..._message_status_sent_from_other_device.xml | 4 + .../src/main/res/drawable/ic_multi_device.xml | 5 + .../res/drawable/ic_pref_remote_delete.xml | 5 + .../main/res/layout/activity_discussion.xml | 1 + .../res/layout/activity_group_creation.xml | 19 +- .../res/layout/activity_group_details.xml | 2 +- .../res/layout/activity_group_v2_details.xml | 2 +- .../res/layout/activity_message_details.xml | 16 + ...ivity_message_details_with_attachments.xml | 16 + .../activity_owned_identity_details.xml | 619 ++-- .../dialog_fragment_identity_deactivated.xml | 42 +- ...agment_publish_cancel_with_placeholder.xml | 2 +- .../layout/dialog_item_osm_attribution.xml | 16 + ...hoice.xml => dialog_item_singlechoice.xml} | 0 .../dialog_view_deactivated_spinner.xml | 21 + .../res/layout/dialog_view_delete_profile.xml | 8 +- .../res/layout/dialog_view_rename_device.xml | 31 + .../fragment_group_creation_group_name.xml | 91 +- .../res/layout/fragment_group_details.xml | 16 +- ...ment_plus_button_configuration_scanned.xml | 17 +- .../layout/fragment_subscription_status.xml | 155 +- .../res/layout/item_view_owned_device.xml | 131 + .../item_view_selectable_owned_device.xml | 90 + .../app/src/main/res/values-fr/strings.xml | 107 +- .../app/src/main/res/values/arrays.xml | 11 + .../app/src/main/res/values/colors.xml | 1 + .../app/src/main/res/values/strings.xml | 107 +- ...ment_preferences_group_custom_settings.xml | 64 + .../main/res/xml/network_security_config.xml | 9 + .../olvid/messenger/billing/BillingUtils.java | 7 +- .../discussion/location/LatLngWrapper.java | 3 +- .../sync/OwnedIdentitySyncSnapshotTest.java | 60 + obv_messenger/gradle.properties | 6 +- 330 files changed, 25938 insertions(+), 9001 deletions(-) rename obv_engine/engine/src/main/java/io/olvid/engine/{metamanager/ExternalNotificationDelegate.java => datatypes/NoAcceptableChannelException.java} (88%) create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/engine/types/JsonGroupType.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvDeviceList.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvDeviceManagementRequest.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvGroupOwnerAndUidKey.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvPushNotificationType.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/engine/types/RegisterApiKeyResult.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/engine/types/SimpleEngineNotificationListener.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/engine/types/identities/ObvOwnedDevice.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvBackupAndSyncDelegate.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvSyncAtom.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvSyncDiff.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvSyncSnapshot.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvSyncSnapshotNode.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/ContactSyncSnapshot.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/GroupV1SyncSnapshot.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/GroupV2SyncSnapshot.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/IdentityDetailsSyncSnapshot.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/IdentityManagerSyncSnapshot.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/KeycloakSyncSnapshot.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/OwnedIdentitySyncSnapshot.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/metamanager/EngineOwnedIdentityCleanupDelegate.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/StandaloneServerQueryOperation.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/ChannelCreationWithOwnedDeviceProtocol.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OwnedDeviceDiscoveryProtocol.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OwnedDeviceManagementProtocol.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OwnedIdentityDeletionProtocol.java delete mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OwnedIdentityDeletionWithContactNotificationProtocol.java create mode 100644 obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/SynchronizationProtocol.java create mode 100644 obv_messenger/app/schemas/io.olvid.messenger.databases.AppDatabase/64.json create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/AppBackupAndSyncDelegate.java delete mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/EngineNotificationProcessorForGroupsV2.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/EngineNotificationProcessorForGroupsV2.kt delete mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/activities/GroupCreationActivity.java delete mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/activities/GroupDetailsActivity.java delete mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/activities/GroupV2DetailsActivity.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/dao/OwnedDeviceDao.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/OwnedDevice.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonDeleteDiscussion.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonDeleteMessages.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonDiscussionRead.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonExpiration.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonLimitedVisibilityMessageOpened.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonLocation.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonMessage.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonMessageReference.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonOneToOneMessageIdentifier.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonPayload.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonQuerySharedSettings.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonReaction.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonReturnReceipt.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonScreenCaptureDetection.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonSharedSettings.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonUpdateMessage.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonUserMention.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/jsons/JsonWebrtcMessage.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/sync/AppSyncSnapshot.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/sync/ContactSyncSnapshot.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/sync/DiscussionCustomizationSyncSnapshot.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/sync/GroupV1SyncSnapshot.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/sync/GroupV2SyncSnapshot.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/sync/OwnedIdentitySyncSnapshot.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/entity/sync/SettingsSyncSnapshot.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/ApplySyncAtomTask.kt create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/OwnedDevicesSynchronisationWithEngineTask.kt create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/databases/tasks/PropagatePinnedDiscussionsChangeTask.kt delete mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/fragments/dialog/GroupV2MemberAdditionDialogFragment.java create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/group/EditOwnedGroupDetailsDialogFragment.kt create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/group/GroupCreationActivity.kt create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/group/GroupCreationViewModel.kt create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/group/GroupCustomSettingsPreferenceFragment.kt create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/group/GroupDetailsActivity.kt create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/group/GroupDetailsViewModel.kt create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/group/GroupTypeModel.kt create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/group/GroupV2DetailsActivity.kt create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/group/GroupV2DetailsViewModel.kt create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/group/GroupV2MemberAdditionDialogFragment.kt create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/group/OwnedGroupDetailsFragment.kt create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/group/OwnedGroupDetailsViewModel.kt create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/group/components/GroupAdminsSelectionDialog.kt create mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/group/components/GroupTypeSelection.kt delete mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/owneddetails/EditOwnedGroupDetailsDialogFragment.java delete mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/owneddetails/OwnedGroupDetailsFragment.java delete mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/owneddetails/OwnedGroupDetailsViewModel.java delete mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/viewModels/GroupCreationViewModel.java delete mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/viewModels/GroupDetailsViewModel.java delete mode 100644 obv_messenger/app/src/main/java/io/olvid/messenger/viewModels/GroupV2DetailsViewModel.java create mode 100644 obv_messenger/app/src/main/res/drawable/ic_device.xml create mode 100644 obv_messenger/app/src/main/res/drawable/ic_device_expiration.xml create mode 100644 obv_messenger/app/src/main/res/drawable/ic_device_inactive.xml create mode 100644 obv_messenger/app/src/main/res/drawable/ic_message_status_sent_from_other_device.xml create mode 100644 obv_messenger/app/src/main/res/drawable/ic_multi_device.xml create mode 100644 obv_messenger/app/src/main/res/drawable/ic_pref_remote_delete.xml create mode 100644 obv_messenger/app/src/main/res/layout/dialog_item_osm_attribution.xml rename obv_messenger/app/src/main/res/layout/{dialog_singlechoice.xml => dialog_item_singlechoice.xml} (100%) create mode 100644 obv_messenger/app/src/main/res/layout/dialog_view_deactivated_spinner.xml create mode 100644 obv_messenger/app/src/main/res/layout/dialog_view_rename_device.xml create mode 100644 obv_messenger/app/src/main/res/layout/item_view_owned_device.xml create mode 100644 obv_messenger/app/src/main/res/layout/item_view_selectable_owned_device.xml create mode 100644 obv_messenger/app/src/main/res/xml/fragment_preferences_group_custom_settings.xml create mode 100644 obv_messenger/app/src/main/res/xml/network_security_config.xml create mode 100644 obv_messenger/app/src/test/java/io/olvid/messenger/databases/entity/sync/OwnedIdentitySyncSnapshotTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 6447fe95..8fe622bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +# Build 207 (0.15.0) +2023-07-31 + +- Minor fixes for group types + +# ~~Build 206 (0.15.0)~~ +2023-07-28 + +- introduction of group "types" allowing, for example, the creation of read-only discussions +- preliminary support for multi-device (device re-synchronization not fully implemented yet) + # Build 205 (0.14.2) 2023-07-13 @@ -14,7 +25,6 @@ - Some markdown fixes - Fix discussion background image rotation issue - # ~~Build 203 (0.14.2)~~ 2023-06-05 diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/backup/BackupManager.java b/obv_engine/engine/src/main/java/io/olvid/engine/backup/BackupManager.java index 8d4d3553..47034171 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/backup/BackupManager.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/backup/BackupManager.java @@ -34,8 +34,6 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.zip.Deflater; -import java.util.zip.DeflaterOutputStream; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; @@ -50,7 +48,6 @@ import io.olvid.engine.datatypes.BackupSeed; import io.olvid.engine.datatypes.Constants; import io.olvid.engine.datatypes.EncryptedBytes; -import io.olvid.engine.datatypes.Identity; import io.olvid.engine.datatypes.NoExceptionSingleThreadExecutor; import io.olvid.engine.datatypes.NotificationListener; import io.olvid.engine.datatypes.Session; @@ -410,15 +407,15 @@ public void backupSuccess(String tag, UID backupKeyUid, int version, String back EncryptionPublicKey encryptionPublicKey = backupKey.getEncryptionPublicKey(); MACKey macKey = backupKey.getMacKey(); - // TODO: once all olvid clients can handle uncompressed backups, remove the DeflaterOutputStream - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - DeflaterOutputStream deflater = new DeflaterOutputStream(baos, new Deflater(5, true)); - deflater.write(fullBackupContent.getBytes(StandardCharsets.UTF_8)); - deflater.close(); - byte[] compressedBackup = baos.toByteArray(); - baos.close(); + // we no longer compress backups as all up-to-date Olvid clients can handle it. +// ByteArrayOutputStream baos = new ByteArrayOutputStream(); +// DeflaterOutputStream deflater = new DeflaterOutputStream(baos, new Deflater(5, true)); +// deflater.write(fullBackupContent.getBytes(StandardCharsets.UTF_8)); +// deflater.close(); +// byte[] compressedBackup = baos.toByteArray(); +// baos.close(); - EncryptedBytes encryptedBackup = Suite.getPublicKeyEncryption(encryptionPublicKey).encrypt(encryptionPublicKey, compressedBackup, prng); + EncryptedBytes encryptedBackup = Suite.getPublicKeyEncryption(encryptionPublicKey).encrypt(encryptionPublicKey, fullBackupContent.getBytes(StandardCharsets.UTF_8), prng); byte[] mac = Suite.getMAC(macKey).digest(macKey, encryptedBackup.getBytes()); byte[] macedEncryptedBackup = new byte[encryptedBackup.getBytes().length + mac.length]; @@ -616,7 +613,7 @@ private BackupContentAndDerivedKeys decryptBackupContent(String seedString, byte } @Override - public ObvIdentity[] restoreOwnedIdentitiesFromBackup(String seedString, byte[] backupContent) { + public ObvIdentity[] restoreOwnedIdentitiesFromBackup(String seedString, byte[] backupContent, String deviceDisplayName) { try { BackupContentAndDerivedKeys backupContentAndDerivedKeys = decryptBackupContent(seedString, backupContent, jsonObjectMapper); if (backupContentAndDerivedKeys == null) { @@ -637,7 +634,8 @@ public ObvIdentity[] restoreOwnedIdentitiesFromBackup(String seedString, byte[] backupManagerSession.session.commit(); } } - return identityDelegate.restoreOwnedIdentitiesFromBackup(backupContentAndDerivedKeys.pojo.engine.identity_manager, prng); + + return identityDelegate.restoreOwnedIdentitiesFromBackup(backupContentAndDerivedKeys.pojo.engine.identity_manager, deviceDisplayName, prng); } catch (Exception e) { e.printStackTrace(); return null; @@ -646,7 +644,7 @@ public ObvIdentity[] restoreOwnedIdentitiesFromBackup(String seedString, byte[] @Override - public void restoreContactsAndGroupsFromBackup(String seedString, byte[] backupContent, Identity[] restoredOwnedIdentities) { + public void restoreContactsAndGroupsFromBackup(String seedString, byte[] backupContent, ObvIdentity[] restoredOwnedIdentities) { try { BackupContentAndDerivedKeys backupContentAndDerivedKeys = decryptBackupContent(seedString, backupContent, jsonObjectMapper); if (backupContentAndDerivedKeys == null) { diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/channel/ChannelManager.java b/obv_engine/engine/src/main/java/io/olvid/engine/channel/ChannelManager.java index a17d7213..01b512ec 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/channel/ChannelManager.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/channel/ChannelManager.java @@ -21,8 +21,11 @@ import java.sql.SQLException; +import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; import io.olvid.engine.Logger; @@ -106,10 +109,19 @@ public void initialisationComplete() { // at the same time, try to detect contact devices without a channel ObliviousChannel[] obliviousChannels = ObliviousChannel.getAll(channelManagerSession); Map>> deviceUidsMap = identityDelegate.getAllDeviceUidsOfAllContactsOfAllOwnedIdentities(channelManagerSession.session); + for (Identity ownedIdentity : identityDelegate.getOwnedIdentities(channelManagerSession.session)) { + UID[] ownedDeviceUids = identityDelegate.getDeviceUidsOfOwnedIdentity(channelManagerSession.session, ownedIdentity); + if (!deviceUidsMap.containsKey(ownedIdentity)) { + deviceUidsMap.put(ownedIdentity, new HashMap<>()); + } + Map> ownedIdentityMap = deviceUidsMap.get(ownedIdentity); + ownedIdentityMap.put(ownedIdentity, new HashSet<>(Arrays.asList(ownedDeviceUids))); + } + for (ObliviousChannel obliviousChannel : obliviousChannels) { Identity ownedIdentity = ownedIdentityFromDeviceUid.get(obliviousChannel.getCurrentDeviceUid()); if (ownedIdentity == null) { - ownedIdentity = identityDelegate.getOwnedIdentityForDeviceUid(channelManagerSession.session, obliviousChannel.getCurrentDeviceUid()); + ownedIdentity = identityDelegate.getOwnedIdentityForCurrentDeviceUid(channelManagerSession.session, obliviousChannel.getCurrentDeviceUid()); if (ownedIdentity == null) { continue; } @@ -130,17 +142,48 @@ public void initialisationComplete() { } } - // now that we have removed all devices for which we have a channel, we walk through the deviceUidsMap to check for channel-less deviceUids + // now that we have removed (from the HashMap) all devices for which we have a channel, we walk through the deviceUidsMap to check for channel-less deviceUids for (Identity ownedIdentity: deviceUidsMap.keySet()) { + // first check if some channels with owned devices should be restarted + boolean deviceDiscoveryNeeded = false; + if (identityDelegate.isActiveOwnedIdentity(channelManagerSession.session, ownedIdentity)) { + for (UID ownedDeviceUid : identityDelegate.getOtherDeviceUidsOfOwnedIdentity(channelManagerSession.session, ownedIdentity)) { + try { + boolean channelExists = checkIfObliviousChannelExists(channelManagerSession.session, ownedIdentity, ownedDeviceUid, ownedIdentity); + boolean channelCreationInProgress = protocolDelegate.isChannelCreationInProgress(channelManagerSession.session, ownedIdentity, ownedIdentity, ownedDeviceUid); + if (!channelExists && !channelCreationInProgress) { + // we found a device without a channel and no channel creation is in progress + // --> we delete the device and start a device discovery protocol + Logger.i("Found an owned device with no channel and no channel creation. Restarting device discovery."); + identityDelegate.removeDeviceForOwnedIdentity(channelManagerSession.session, ownedIdentity, ownedDeviceUid); + deviceDiscoveryNeeded = true; + } + } catch (Exception e) { + // nothing to do + } + } + if (deviceDiscoveryNeeded) { + try { + protocolStarterDelegate.startOwnedDeviceDiscoveryProtocolWithinTransaction(channelManagerSession.session, ownedIdentity); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + Map> ownedIdentityMap = deviceUidsMap.get(ownedIdentity); if (ownedIdentityMap == null) { continue; } for (Identity contactIdentity: ownedIdentityMap.keySet()) { + if (contactIdentity.equals(ownedIdentity)) { + continue; + } Set deviceUidSet = ownedIdentityMap.get(contactIdentity); if (deviceUidSet == null) { continue; } + deviceDiscoveryNeeded = false; for (UID contactDeviceUid: deviceUidSet) { // check if a ChannelCreationProtocolInstance exists for this device try { @@ -149,15 +192,23 @@ public void initialisationComplete() { // --> we delete the device and start a device discovery protocol Logger.i("Found a contact device with no channel and no channel creation. Restarting device discovery."); identityDelegate.removeDeviceForContactIdentity(channelManagerSession.session, ownedIdentity, contactIdentity, contactDeviceUid); - protocolStarterDelegate.startDeviceDiscoveryProtocolWithinTransaction(channelManagerSession.session, ownedIdentity, contactIdentity); + deviceDiscoveryNeeded = true; } } catch (Exception e) { // nothing to do } } + if (deviceDiscoveryNeeded) { + try { + protocolStarterDelegate.startDeviceDiscoveryProtocolWithinTransaction(channelManagerSession.session, ownedIdentity, contactIdentity); + } catch (Exception e) { + e.printStackTrace(); + } + } } } } + channelManagerSession.session.commit(); } catch (SQLException e) { e.printStackTrace(); @@ -282,7 +333,12 @@ private ObliviousChannel getObliviousChannel(Session session, Identity ownedIden @Override public UID[] getConfirmedObliviousChannelDeviceUids(Session session, Identity ownedIdentity, Identity remoteIdentity) throws Exception { - UID[] remoteUids = identityDelegate.getDeviceUidsOfContactIdentity(session, ownedIdentity, remoteIdentity); + UID[] remoteUids; + if (Objects.equals(ownedIdentity, remoteIdentity)) { // channels with owned devices + remoteUids = identityDelegate.getOtherDeviceUidsOfOwnedIdentity(session, ownedIdentity); + } else { + remoteUids = identityDelegate.getDeviceUidsOfContactIdentity(session, ownedIdentity, remoteIdentity); + } if ((remoteUids == null) || (remoteUids.length == 0)) { return new UID[0]; } @@ -308,24 +364,31 @@ public void deleteObliviousChannelsWithContact(Session session, Identity ownedId @Override public void deleteObliviousChannelIfItExists(Session session, Identity ownedIdentity, UID remoteDeviceUid, Identity remoteIdentity) throws Exception { UID currentDeviceUid = identityDelegate.getCurrentDeviceUidOfOwnedIdentity(session, ownedIdentity); - // delete the channel ObliviousChannel obliviousChannel = ObliviousChannel.get(wrapSession(session), currentDeviceUid, remoteDeviceUid, remoteIdentity, false); if (obliviousChannel != null) { + // delete the channel obliviousChannel.delete(); } } - public boolean checkIfObliviousChannelExists(Session session, Identity ownedIdentity, UID remoteDeviceUid, Identity remoteIdentity) throws Exception { + @Override + public void deleteAllChannelsForOwnedIdentity(Session session, Identity ownedIdentity) throws SQLException { + UID currentDeviceUid = identityDelegate.getCurrentDeviceUidOfOwnedIdentity(session, ownedIdentity); + ObliviousChannel.deleteAll(wrapSession(session), currentDeviceUid); + } + + public boolean checkIfObliviousChannelExists(Session session, Identity ownedIdentity, UID remoteDeviceUid, Identity remoteIdentity) throws SQLException { UID currentDeviceUid = identityDelegate.getCurrentDeviceUidOfOwnedIdentity(session, ownedIdentity); - // delete the channel ObliviousChannel obliviousChannel = ObliviousChannel.get(wrapSession(session), currentDeviceUid, remoteDeviceUid, remoteIdentity, false); return obliviousChannel != null; } @Override - public void deleteAllChannelsForOwnedIdentity(Session session, Identity ownedIdentity) throws SQLException { + public boolean checkIfObliviousChannelIsConfirmed(Session session, Identity ownedIdentity, UID remoteDeviceUid, Identity remoteIdentity) throws SQLException { UID currentDeviceUid = identityDelegate.getCurrentDeviceUidOfOwnedIdentity(session, ownedIdentity); - ObliviousChannel.deleteAll(wrapSession(session), currentDeviceUid); + ObliviousChannel obliviousChannel = ObliviousChannel.get(wrapSession(session), currentDeviceUid, remoteDeviceUid, remoteIdentity, true); + return obliviousChannel != null; + } // endregion diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/channel/databases/ObliviousChannel.java b/obv_engine/engine/src/main/java/io/olvid/engine/channel/databases/ObliviousChannel.java index f38799d6..63970ec3 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/channel/databases/ObliviousChannel.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/channel/databases/ObliviousChannel.java @@ -29,6 +29,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Objects; import io.olvid.engine.Logger; import io.olvid.engine.channel.datatypes.AuthEncKeyAndChannelInfo; @@ -421,7 +422,7 @@ public static ObliviousChannel get(ChannelManagerSession channelManagerSession, return null; } try (PreparedStatement statement = channelManagerSession.session.prepareStatement("SELECT * FROM " + TABLE_NAME + " WHERE " + - (necessarilyConfirmed ? (CONFIRMED + " = 1 AND "): "") + + (necessarilyConfirmed ? (CONFIRMED + " = 1 AND ") : "") + CURRENT_DEVICE_UID + " = ? AND " + REMOTE_DEVICE_UID + " = ? AND " + REMOTE_IDENTITY + " = ?;")) { statement.setBytes(1, currentDeviceUid.getBytes()); statement.setBytes(2, remoteDeviceUid.getBytes()); @@ -469,31 +470,13 @@ public static ObliviousChannel[] getAllConfirmed(ChannelManagerSession channelMa } } - public static int countConfirmedWithContact(ChannelManagerSession channelManagerSession, UID currentDeviceUid, Identity remoteIdentity) throws SQLException { - try (PreparedStatement statement = channelManagerSession.session.prepareStatement("SELECT count(*) FROM " + TABLE_NAME + - " WHERE " + CONFIRMED + " = 1" + - " AND " + CURRENT_DEVICE_UID + " = ? " + - " AND " + REMOTE_IDENTITY + " = ?;")) { - statement.setBytes(1, currentDeviceUid.getBytes()); - statement.setBytes(2, remoteIdentity.getBytes()); - try (ResultSet res = statement.executeQuery()) { - if (res.next()) { - return res.getInt(1); - } - return 0; - } - } - } - - - public static ObliviousChannel[] getMany(ChannelManagerSession channelManagerSession, UID currentDeviceUid, UID[] remoteDeviceUids, Identity remoteIdentity, boolean necessarilyConfirmed) { if ((currentDeviceUid == null) || (remoteDeviceUids == null) || (remoteDeviceUids.length == 0) || (remoteIdentity == null)) { return null; } String questionMarks = "("; - for (int i=0; i list = new ArrayList<>(); @@ -671,7 +654,14 @@ public static ObliviousChannel[] acceptableChannelsForPosting(ChannelManagerSess // Only protocol messages may be sent through unconfirmed channels return new ObliviousChannel[0]; } - HashSet remoteDeviceUidSet = new HashSet<>(Arrays.asList(channelManagerSession.identityDelegate.getDeviceUidsOfContactIdentity(channelManagerSession.session, message.getSendChannelInfo().getFromIdentity(), message.getSendChannelInfo().getToIdentity()))); + HashSet remoteDeviceUidSet; + if (Objects.equals(message.getSendChannelInfo().getFromIdentity(), message.getSendChannelInfo().getToIdentity())) { + // posting for owned identity + remoteDeviceUidSet = new HashSet<>(Arrays.asList(channelManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(channelManagerSession.session, message.getSendChannelInfo().getFromIdentity()))); + } else { + // posting for contact + remoteDeviceUidSet = new HashSet<>(Arrays.asList(channelManagerSession.identityDelegate.getDeviceUidsOfContactIdentity(channelManagerSession.session, message.getSendChannelInfo().getFromIdentity(), message.getSendChannelInfo().getToIdentity()))); + } remoteDeviceUidSet.retainAll(Arrays.asList(message.getSendChannelInfo().getRemoteDeviceUids())); UID currentDeviceUid = channelManagerSession.identityDelegate.getCurrentDeviceUidOfOwnedIdentity(channelManagerSession.session, message.getSendChannelInfo().getFromIdentity()); @@ -690,7 +680,12 @@ public static ObliviousChannel[] acceptableChannelsForPosting(ChannelManagerSess List acceptableChannels = new ArrayList<>(); UID currentDeviceUid = channelManagerSession.identityDelegate.getCurrentDeviceUidOfOwnedIdentity(channelManagerSession.session, message.getSendChannelInfo().getFromIdentity()); for (Identity toIdentity: message.getSendChannelInfo().getToIdentities()) { - UID[] remoteDeviceUids = channelManagerSession.identityDelegate.getDeviceUidsOfContactIdentity(channelManagerSession.session, message.getSendChannelInfo().getFromIdentity(), toIdentity); + final UID[] remoteDeviceUids; + if (Objects.equals(message.getSendChannelInfo().getFromIdentity(), toIdentity)) { + remoteDeviceUids = channelManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(channelManagerSession.session, message.getSendChannelInfo().getFromIdentity()); + } else { + remoteDeviceUids = channelManagerSession.identityDelegate.getDeviceUidsOfContactIdentity(channelManagerSession.session, message.getSendChannelInfo().getFromIdentity(), toIdentity); + } acceptableChannels.addAll(Arrays.asList(ObliviousChannel.getAcceptableObliviousChannels(channelManagerSession, currentDeviceUid, remoteDeviceUids, toIdentity, true))); } return acceptableChannels.toArray(new ObliviousChannel[0]); diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/channel/datatypes/NetworkChannel.java b/obv_engine/engine/src/main/java/io/olvid/engine/channel/datatypes/NetworkChannel.java index 8b68528c..8c04c774 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/channel/datatypes/NetworkChannel.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/channel/datatypes/NetworkChannel.java @@ -27,6 +27,7 @@ import io.olvid.engine.crypto.PRNGService; import io.olvid.engine.crypto.Suite; import io.olvid.engine.datatypes.EncryptedBytes; +import io.olvid.engine.datatypes.NoAcceptableChannelException; import io.olvid.engine.datatypes.Seed; import io.olvid.engine.datatypes.UID; import io.olvid.engine.datatypes.containers.ChannelApplicationMessageToSend; @@ -66,7 +67,7 @@ public static UID post(ChannelManagerSession channelManagerSession, ChannelMessa if (networkChannels.length == 0) { Logger.i("No acceptable channels were found for posting"); - throw new Exception(); + throw new NoAcceptableChannelException(); } // get the minimum suite version of all network channels @@ -125,6 +126,7 @@ public static UID post(ChannelManagerSession channelManagerSession, ChannelMessa }); //////// // Add a padding to message to obfuscate content length. Commented out for now + // TODO: uncomment once all clients can handle padded messages // byte[] paddedPlaintext = new byte[((plaintextContent.getBytes().length - 1) | 511) + 1]; // System.arraycopy(plaintextContent.getBytes(), 0, paddedPlaintext, 0, plaintextContent.getBytes().length); // EncryptedBytes encryptedContent = authEnc.encrypt(messageKey, paddedPlaintext, prng); @@ -153,6 +155,7 @@ public static UID post(ChannelManagerSession channelManagerSession, ChannelMessa }); //////// // Add a padding to message to obfuscate content length. Commented out for now + // TODO: uncomment once all clients can handle padded messages // byte[] paddedPlaintext = new byte[((plaintextContent.getBytes().length - 1) | 511) + 1]; // System.arraycopy(plaintextContent.getBytes(), 0, paddedPlaintext, 0, plaintextContent.getBytes().length); // EncryptedBytes encryptedContent = authEnc.encrypt(messageKey, paddedPlaintext, prng); diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/Constants.java b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/Constants.java index 1ad5125b..b7609af8 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/Constants.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/Constants.java @@ -23,8 +23,8 @@ import java.nio.charset.StandardCharsets; public abstract class Constants { - public static final int CURRENT_ENGINE_DB_SCHEMA_VERSION = 34; - public static final int SERVER_API_VERSION = 13; + public static final int CURRENT_ENGINE_DB_SCHEMA_VERSION = 35; + public static final int SERVER_API_VERSION = 15; public static final int CURRENT_BACKUP_JSON_VERSION = 0; // files / folders @@ -49,6 +49,7 @@ public abstract class Constants { @SuppressWarnings("PointlessBitwiseExpression") public static final long API_KEY_PERMISSION_CALL = 1L << 0; public static final long API_KEY_PERMISSION_WEB_CLIENT = 1L << 1; + public static final long API_KEY_PERMISSION_MULTI_DEVICE = 1L << 2; // full ratcheting thresholds @@ -71,6 +72,7 @@ public abstract class Constants { // backups public static final long AUTOBACKUP_MAX_INTERVAL = 86_400_000L; // 1 day public static final long AUTOBACKUP_START_DELAY = 60_000L * 2; // 2 minutes + public static final long PERIODIC_OWNED_DEVICE_SYNC_INTERVAL = 86_400_000L; // 1 day public static final int SERVER_SESSION_NONCE_LENGTH = 32; public static final int SERVER_SESSION_CHALLENGE_LENGTH = 32; @@ -93,6 +95,7 @@ public abstract class Constants { public static final long BASE_RESCHEDULING_TIME = 250L; public static final long WEBSOCKET_PING_INTERVAL_MILLIS = 20_000L; + public static final long NO_DEVICE_CONTACT_DEVICE_DISCOVERY_INTERVAL = 3 * 86_400_000; // Keycloak public static final long KEYCLOAK_SIGNATURE_VALIDITY_MILLIS = 60 * 86_400_000L; diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/ExponentialBackoffRepeatingScheduler.java b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/ExponentialBackoffRepeatingScheduler.java index 32bfc2e6..f6466f7c 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/ExponentialBackoffRepeatingScheduler.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/ExponentialBackoffRepeatingScheduler.java @@ -116,7 +116,7 @@ public ScheduledFuture scheduleAtFixedRate(Runnable runnable, int i, long del return scheduler.scheduleAtFixedRate(runnable, i, delay, timeUnit); } - private long computeReschedulingDelay(int failedAttemptCount) { + protected long computeReschedulingDelay(int failedAttemptCount) { return (long) ((Constants.BASE_RESCHEDULING_TIME << failedAttemptCount) * (1 + random.nextFloat())); } } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/ExternalNotificationDelegate.java b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/NoAcceptableChannelException.java similarity index 88% rename from obv_engine/engine/src/main/java/io/olvid/engine/metamanager/ExternalNotificationDelegate.java rename to obv_engine/engine/src/main/java/io/olvid/engine/datatypes/NoAcceptableChannelException.java index a8cb6695..e478f01c 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/ExternalNotificationDelegate.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/NoAcceptableChannelException.java @@ -17,9 +17,7 @@ * along with Olvid. If not, see . */ -package io.olvid.engine.metamanager; - - -public interface ExternalNotificationDelegate { +package io.olvid.engine.datatypes; +public class NoAcceptableChannelException extends Exception { } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/PushNotificationTypeAndParameters.java b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/PushNotificationTypeAndParameters.java index a8f4cea7..b6a3e763 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/PushNotificationTypeAndParameters.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/PushNotificationTypeAndParameters.java @@ -23,44 +23,61 @@ public class PushNotificationTypeAndParameters { public static final byte PUSH_NOTIFICATION_TYPE_ANDROID = 0x01; - public static final byte PUSH_NOTIFICATION_TYPE_NONE = (byte) 0xff; + public static final byte PUSH_NOTIFICATION_TYPE_WEBSOCKET_ANDROID = 0x10; + public static final byte PUSH_NOTIFICATION_TYPE_WEBSOCKET_WINDOWS = 0x11; + public static final byte PUSH_NOTIFICATION_TYPE_WEBSOCKET_LINUX = 0x12; + public static final byte PUSH_NOTIFICATION_TYPE_WEBSOCKET_DAEMON = 0x13; + + // public static final byte PUSH_NOTIFICATION_TYPE_NONE = (byte) 0xff; public final byte pushNotificationType; public final byte[] token; public UID identityMaskingUid; // not taken into account when comparing, this is supposed to always be random. Not final so we can sometimes copy old maskingUid - public boolean kickOtherDevices; // not taken into account when comparing. Not final so we can sometimes preserve the previous kickMode - public final boolean useMultiDevice; + public boolean reactivateCurrentDevice; // not taken into account when comparing. Not final so we can sometimes preserve the previous kickMode + public UID deviceUidToReplace; // deviceUID of the device to deactivate when reactivateCurrentDevice is set. - public PushNotificationTypeAndParameters(byte pushNotificationType, byte[] token, UID identityMaskingUid, boolean kickOtherDevices, boolean useMultiDevice) { + public PushNotificationTypeAndParameters(byte pushNotificationType, byte[] token, UID identityMaskingUid, boolean reactivateCurrentDevice, UID deviceUidToReplace) { this.pushNotificationType = pushNotificationType; this.token = token; this.identityMaskingUid = identityMaskingUid; - this.kickOtherDevices = kickOtherDevices; - this.useMultiDevice = useMultiDevice; + this.reactivateCurrentDevice = reactivateCurrentDevice; + this.deviceUidToReplace = deviceUidToReplace; + } + + public static PushNotificationTypeAndParameters createWebsocketOnlyAndroid(boolean reactivateCurrentDevice, UID deviceUidToReplace) { + return new PushNotificationTypeAndParameters(PUSH_NOTIFICATION_TYPE_WEBSOCKET_ANDROID, null, null, reactivateCurrentDevice, deviceUidToReplace); + } + + public static PushNotificationTypeAndParameters createWindows(boolean reactivateCurrentDevice, UID deviceUidToReplace) { + return new PushNotificationTypeAndParameters(PUSH_NOTIFICATION_TYPE_WEBSOCKET_WINDOWS, null, null, reactivateCurrentDevice, deviceUidToReplace); + } + + public static PushNotificationTypeAndParameters createLinux(boolean reactivateCurrentDevice, UID deviceUidToReplace) { + return new PushNotificationTypeAndParameters(PUSH_NOTIFICATION_TYPE_WEBSOCKET_LINUX, null, null, reactivateCurrentDevice, deviceUidToReplace); } - public static PushNotificationTypeAndParameters createWebsocketOnly(boolean kickOtherDevices, boolean useMultidevice) { - return new PushNotificationTypeAndParameters(PUSH_NOTIFICATION_TYPE_NONE, null, null, kickOtherDevices, useMultidevice); + public static PushNotificationTypeAndParameters createDaemon(boolean reactivateCurrentDevice, UID deviceUidToReplace) { + return new PushNotificationTypeAndParameters(PUSH_NOTIFICATION_TYPE_WEBSOCKET_DAEMON, null, null, reactivateCurrentDevice, deviceUidToReplace); } - public static PushNotificationTypeAndParameters createFirebaseAndroid(byte[] token, UID identityMaskingUid, boolean kickOtherDevices, boolean useMultidevice) { - return new PushNotificationTypeAndParameters(PUSH_NOTIFICATION_TYPE_ANDROID, token, identityMaskingUid, kickOtherDevices, useMultidevice); + public static PushNotificationTypeAndParameters createFirebaseAndroid(byte[] token, UID identityMaskingUid, boolean reactivateCurrentDevice, UID deviceUidToReplace) { + return new PushNotificationTypeAndParameters(PUSH_NOTIFICATION_TYPE_ANDROID, token, identityMaskingUid, reactivateCurrentDevice, deviceUidToReplace); } - @Override - public boolean equals(Object o) { - if (o instanceof PushNotificationTypeAndParameters) { - PushNotificationTypeAndParameters other = (PushNotificationTypeAndParameters) o; - if (pushNotificationType != other.pushNotificationType || useMultiDevice != other.useMultiDevice) { + public boolean sameTypeAndToken(PushNotificationTypeAndParameters other) { + if (pushNotificationType != other.pushNotificationType) { + return false; + } + switch (pushNotificationType) { + case PUSH_NOTIFICATION_TYPE_WEBSOCKET_ANDROID: + case PUSH_NOTIFICATION_TYPE_WEBSOCKET_WINDOWS: + case PUSH_NOTIFICATION_TYPE_WEBSOCKET_LINUX: + case PUSH_NOTIFICATION_TYPE_WEBSOCKET_DAEMON: + return true; + case PUSH_NOTIFICATION_TYPE_ANDROID: + return Arrays.equals(token, other.token); + default: return false; - } - switch (pushNotificationType) { - case PUSH_NOTIFICATION_TYPE_NONE: - return true; - case PUSH_NOTIFICATION_TYPE_ANDROID: - return Arrays.equals(token, other.token); - } } - return false; } } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/ServerMethod.java b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/ServerMethod.java index 6e988ab1..e8007dc1 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/ServerMethod.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/ServerMethod.java @@ -59,6 +59,7 @@ public abstract class ServerMethod { public static final byte GROUP_IS_LOCKED = 0x13; public static final byte INVALID_SIGNATURE = 0x14; public static final byte GROUP_NOT_LOCKED = 0x15; + public static final byte INVALID_API_KEY = 0x16; public static final byte GENERAL_ERROR = (byte) 0xff; diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/Session.java b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/Session.java index 4d9c20d9..accd6a72 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/Session.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/Session.java @@ -182,7 +182,7 @@ public PreparedStatement prepareStatement(String s, int i) throws SQLException { @Override public void close() throws SQLException { if (!sessionCommitListeners.isEmpty() ) { - Logger.e("This Session cannot yet be closed: some modifications were committed and the corresponding hooks have not been called."); + Logger.e("This Session was not properly closed: some modifications were committed and the corresponding hooks have not been called."); for (SessionCommitListener sessionCommitListener: sessionCommitListeners) { Logger.e("Not committed entity: " + sessionCommitListener.getClass()); } @@ -488,7 +488,16 @@ public ResultSet executeQuery(String s) throws SQLException { @Override public int executeUpdate(final String s) throws SQLException { - throw new SQLException("Not implemented"); + if (session.getAutoCommit()) { + try { + Session.globalWriteLock.lock(); + return statement.executeUpdate(s); + } finally { + Session.globalWriteLock.unlock(); + } + } else { + return statement.executeUpdate(s); + } } @Override diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/containers/DialogType.java b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/containers/DialogType.java index 2d53c385..7f096bc0 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/containers/DialogType.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/containers/DialogType.java @@ -22,6 +22,7 @@ import io.olvid.engine.datatypes.Identity; import io.olvid.engine.datatypes.UID; +import io.olvid.engine.engine.types.sync.ObvSyncAtom; import io.olvid.engine.engine.types.identities.ObvGroupV2; public class DialogType { @@ -43,6 +44,7 @@ public class DialogType { public static final int ACCEPT_ONE_TO_ONE_INVITATION_DIALOG_ID = 14; public static final int ACCEPT_GROUP_V2_INVITATION_DIALOG_ID = 15; public static final int GROUP_V2_FROZEN_INVITATION_DIALOG_ID = 16; + public static final int SYNC_ITEM_TO_APPLY_DIALOG_ID = 17; public final int id; @@ -57,8 +59,9 @@ public class DialogType { public final String[] pendingGroupMemberSerializedDetails; public final Long serverTimestamp; public final ObvGroupV2 obvGroupV2; + public final ObvSyncAtom obvSyncAtom; - private DialogType(int id, String contactDisplayNameOrSerializedDetails, Identity contactIdentity, byte[] sasToDisplay, byte[] sasEntered, Identity mediatorOrGroupOwnerIdentity, String serializedGroupDetails, UID groupUid, Identity[] pendingGroupMemberIdentities, String[] pendingGroupMemberSerializedDetails, Long serverTimestamp, ObvGroupV2 obvGroupV2) { + private DialogType(int id, String contactDisplayNameOrSerializedDetails, Identity contactIdentity, byte[] sasToDisplay, byte[] sasEntered, Identity mediatorOrGroupOwnerIdentity, String serializedGroupDetails, UID groupUid, Identity[] pendingGroupMemberIdentities, String[] pendingGroupMemberSerializedDetails, Long serverTimestamp, ObvGroupV2 obvGroupV2, ObvSyncAtom obvSyncAtom) { this.id = id; this.contactDisplayNameOrSerializedDetails = contactDisplayNameOrSerializedDetails; this.contactIdentity = contactIdentity; @@ -71,57 +74,62 @@ private DialogType(int id, String contactDisplayNameOrSerializedDetails, Identit this.pendingGroupMemberSerializedDetails = pendingGroupMemberSerializedDetails; this.serverTimestamp = serverTimestamp; this.obvGroupV2 = obvGroupV2; + this.obvSyncAtom = obvSyncAtom; } public static DialogType createDeleteDialog() { - return new DialogType(DELETE_DIALOG_ID, null, null, null, null, null, null, null, null, null, null, null); + return new DialogType(DELETE_DIALOG_ID, null, null, null, null, null, null, null, null, null, null, null, null); } public static DialogType createInviteSentDialog(String contactDisplayName, Identity contactIdentity) { - return new DialogType(INVITE_SENT_DIALOG_ID, contactDisplayName, contactIdentity, null, null, null, null, null, null, null, null, null); + return new DialogType(INVITE_SENT_DIALOG_ID, contactDisplayName, contactIdentity, null, null, null, null, null, null, null, null, null, null); } public static DialogType createAcceptInviteDialog(String contactSerializedDetails, Identity contactIdentity, long serverTimestamp) { - return new DialogType(ACCEPT_INVITE_DIALOG_ID, contactSerializedDetails, contactIdentity, null, null, null, null, null, null, null, serverTimestamp, null); + return new DialogType(ACCEPT_INVITE_DIALOG_ID, contactSerializedDetails, contactIdentity, null, null, null, null, null, null, null, serverTimestamp, null, null); } public static DialogType createSasExchangeDialog(String contactSerializedDetails, Identity contactIdentity, byte[] sasToDisplay, long serverTimestamp) { - return new DialogType(SAS_EXCHANGE_DIALOG_ID, contactSerializedDetails, contactIdentity, sasToDisplay, null, null, null, null, null, null, serverTimestamp, null); + return new DialogType(SAS_EXCHANGE_DIALOG_ID, contactSerializedDetails, contactIdentity, sasToDisplay, null, null, null, null, null, null, serverTimestamp, null, null); } public static DialogType createSasConfirmedDialog(String contactSerializedDetails, Identity contactIdentity, byte[] sasToDisplay, byte[] sasEntered) { - return new DialogType(SAS_CONFIRMED_DIALOG_ID, contactSerializedDetails, contactIdentity, sasToDisplay, sasEntered, null, null, null, null, null, null, null); + return new DialogType(SAS_CONFIRMED_DIALOG_ID, contactSerializedDetails, contactIdentity, sasToDisplay, sasEntered, null, null, null, null, null, null, null, null); } public static DialogType createInviteAcceptedDialog(String contactSerializedDetails, Identity contactIdentity) { - return new DialogType(INVITE_ACCEPTED_DIALOG_ID, contactSerializedDetails, contactIdentity, null, null, null, null, null, null, null, null, null); + return new DialogType(INVITE_ACCEPTED_DIALOG_ID, contactSerializedDetails, contactIdentity, null, null, null, null, null, null, null, null, null, null); } public static DialogType createAcceptMediatorInviteDialog(String contactSerializedDetails, Identity contactIdentity, Identity mediatorIdentity, long serverTimestamp) { - return new DialogType(ACCEPT_MEDIATOR_INVITE_DIALOG_ID, contactSerializedDetails, contactIdentity, null, null, mediatorIdentity, null, null, null, null, serverTimestamp, null); + return new DialogType(ACCEPT_MEDIATOR_INVITE_DIALOG_ID, contactSerializedDetails, contactIdentity, null, null, mediatorIdentity, null, null, null, null, serverTimestamp, null, null); } public static DialogType createMediatorInviteAcceptedDialog(String contactSerializedDetails, Identity contactIdentity, Identity mediatorIdentity) { - return new DialogType(MEDIATOR_INVITE_ACCEPTED_DIALOG_ID, contactSerializedDetails, contactIdentity, null, null, mediatorIdentity, null, null, null, null, null, null); + return new DialogType(MEDIATOR_INVITE_ACCEPTED_DIALOG_ID, contactSerializedDetails, contactIdentity, null, null, mediatorIdentity, null, null, null, null, null, null, null); } public static DialogType createAcceptGroupInviteDialog(String serializedGroupDetails, UID groupUid, Identity groupOwnerIdentity, Identity[] pendingGroupMemberIdentities, String[] pendingGroupMemberSerializedDetails, long serverTimestamp) { - return new DialogType(ACCEPT_GROUP_INVITE_DIALOG_ID, null, null, null, null, groupOwnerIdentity, serializedGroupDetails, groupUid, pendingGroupMemberIdentities, pendingGroupMemberSerializedDetails, serverTimestamp, null); + return new DialogType(ACCEPT_GROUP_INVITE_DIALOG_ID, null, null, null, null, groupOwnerIdentity, serializedGroupDetails, groupUid, pendingGroupMemberIdentities, pendingGroupMemberSerializedDetails, serverTimestamp, null, null); } public static DialogType createOneToOneInvitationSentDialog(Identity contactIdentity) { - return new DialogType(ONE_TO_ONE_INVITATION_SENT_DIALOG_ID, null, contactIdentity, null, null, null, null, null, null, null, null, null); + return new DialogType(ONE_TO_ONE_INVITATION_SENT_DIALOG_ID, null, contactIdentity, null, null, null, null, null, null, null, null, null, null); } public static DialogType createAcceptOneToOneInvitationDialog(Identity contactIdentity, long serverTimestamp) { - return new DialogType(ACCEPT_ONE_TO_ONE_INVITATION_DIALOG_ID, null, contactIdentity, null, null, null, null, null, null, null, serverTimestamp, null); + return new DialogType(ACCEPT_ONE_TO_ONE_INVITATION_DIALOG_ID, null, contactIdentity, null, null, null, null, null, null, null, serverTimestamp, null, null); } public static DialogType createGroupV2InvitationDialog(Identity inviterIdentity, ObvGroupV2 obvGroupV2) { - return new DialogType(ACCEPT_GROUP_V2_INVITATION_DIALOG_ID, null, null, null, null, inviterIdentity, null, null, null, null, null, obvGroupV2); + return new DialogType(ACCEPT_GROUP_V2_INVITATION_DIALOG_ID, null, null, null, null, inviterIdentity, null, null, null, null, null, obvGroupV2, null); } public static DialogType createGroupV2FrozenInvitationDialog(Identity inviterIdentity, ObvGroupV2 obvGroupV2) { - return new DialogType(GROUP_V2_FROZEN_INVITATION_DIALOG_ID, null, null, null, null, inviterIdentity, null, null, null, null, null, obvGroupV2); + return new DialogType(GROUP_V2_FROZEN_INVITATION_DIALOG_ID, null, null, null, null, inviterIdentity, null, null, null, null, null, obvGroupV2, null); + } + + public static DialogType createSyncItemToApplyDialog(ObvSyncAtom obvSyncAtom) { + return new DialogType(SYNC_ITEM_TO_APPLY_DIALOG_ID, null, null, null, null, null, null, null, null, null, null, null, obvSyncAtom); } } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/containers/GroupV2.java b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/containers/GroupV2.java index 36150897..608e6395 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/containers/GroupV2.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/containers/GroupV2.java @@ -313,6 +313,9 @@ public boolean isPrefixedBy(AdministratorsChain prefix) { return true; } + public boolean isChainCreatedBy(Identity identity) { + return blocks.length > 0 && blocks[0].isSignatureValid(new Identity[]{identity}); + } private AdministratorsChain(UID groupUid, Block[] blocks, boolean integrityWasChecked) { this.groupUid = groupUid; @@ -465,19 +468,22 @@ public static class ServerBlob { public static final String KEY_VERSION = "v"; public static final String KEY_SERIALIZED_GROUP_DETAILS = "det"; public static final String KEY_SERVER_PHOTO_INFO = "ph"; + public static final String KEY_SERIALIZED_GROUP_TYPE = "t"; public final AdministratorsChain administratorsChain; public final HashSet groupMemberIdentityAndPermissionsAndDetailsList; public final int version; public final String serializedGroupDetails; public final ServerPhotoInfo serverPhotoInfo; // null if the group does not have a photo + public final String serializedGroupType; - public ServerBlob(AdministratorsChain administratorsChain, HashSet groupMemberIdentityAndPermissionsAndDetailsList, int version, String serializedGroupDetails, ServerPhotoInfo serverPhotoInfo) { + public ServerBlob(AdministratorsChain administratorsChain, HashSet groupMemberIdentityAndPermissionsAndDetailsList, int version, String serializedGroupDetails, ServerPhotoInfo serverPhotoInfo, String serializedGroupType) { this.administratorsChain = administratorsChain; this.groupMemberIdentityAndPermissionsAndDetailsList = groupMemberIdentityAndPermissionsAndDetailsList; this.version = version; this.serializedGroupDetails = serializedGroupDetails; this.serverPhotoInfo = serverPhotoInfo; + this.serializedGroupType = serializedGroupType; } public Encoded encode() { @@ -495,6 +501,9 @@ public Encoded encode() { if (serverPhotoInfo != null) { map.put(new DictionaryKey(KEY_SERVER_PHOTO_INFO), serverPhotoInfo.encode()); } + if (serializedGroupType != null) { + map.put(new DictionaryKey(KEY_SERIALIZED_GROUP_TYPE), Encoded.of(serializedGroupType)); + } return Encoded.of(map); } @@ -532,7 +541,10 @@ public static ServerBlob of(Encoded encoded) throws DecodingException { value = map.get(new DictionaryKey(KEY_SERVER_PHOTO_INFO)); ServerPhotoInfo serverPhotoInfo = value == null ? null : ServerPhotoInfo.of(value); - return new ServerBlob(administratorsChain, groupMemberIdentityAndPermissionsAndDetailsList, version, serializedGroupDetails, serverPhotoInfo); + value = map.get(new DictionaryKey(KEY_SERIALIZED_GROUP_TYPE)); + String serializedGroupType = value == null ? null : value.decodeString(); + + return new ServerBlob(administratorsChain, groupMemberIdentityAndPermissionsAndDetailsList, version, serializedGroupDetails, serverPhotoInfo, serializedGroupType); } public void consolidateWithLogEntries(GroupV2.Identifier groupIdentifier, List logEntries) { @@ -552,8 +564,6 @@ public void consolidateWithLogEntries(GroupV2.Identifier groupIdentifier, List public static final String NOTIFICATION_SERVER_SESSION_CREATED_API_KEY_EXPIRATION_TIMESTAMP_KEY = "api_key_expiration_timestamp"; // long -> 0 means no expiration - public static final String NOTIFICATION_API_KEY_REJECTED_BY_SERVER = "network_fetch_notification_api_key_rejected_by_server"; - public static final String NOTIFICATION_API_KEY_REJECTED_BY_SERVER_IDENTITY_KEY = "identity"; public static final String NOTIFICATION_SERVER_POLLED = "network_fetch_notification_server_polled"; public static final String NOTIFICATION_SERVER_POLLED_OWNED_IDENTITY_KEY = "owned_identity"; @@ -102,6 +100,8 @@ public abstract class DownloadNotifications { public static final String NOTIFICATION_WEBSOCKET_CONNECTION_STATE_CHANGED = "network_fetch_notification_websocket_connection_state_changed"; public static final String NOTIFICATION_WEBSOCKET_CONNECTION_STATE_CHANGED_STATE_KEY = "state"; // int + public static final String NOTIFICATION_WEBSOCKET_DETECTED_SOME_NETWORK = "network_fetch_notification_websocket_detected_some_network"; + public enum TurnCredentialsFailedReason { PERMISSION_DENIED, BAD_SERVER_SESSION, @@ -156,4 +156,7 @@ public enum TurnCredentialsFailedReason { public static final String NOTIFICATION_PUSH_KEYCLOAK_UPDATE_REQUIRED = "network_fetch_notification_keycloak_update_required"; public static final String NOTIFICATION_PUSH_KEYCLOAK_UPDATE_REQUIRED_OWNED_IDENTITY_KEY = "identity"; // Identity + + public static final String NOTIFICATION_PUSH_REGISTER_FAILED_BAD_DEVICE_UID_TO_REPLACE = "network_fetch_notification_push_register_failed_bad_device_uid_to_replace"; + public static final String NOTIFICATION_PUSH_REGISTER_FAILED_BAD_DEVICE_UID_TO_REPLACE_OWNED_IDENTITY_KEY = "owned_identity"; // Identity } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/notifications/IdentityNotifications.java b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/notifications/IdentityNotifications.java index 51c59cbe..22775492 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/notifications/IdentityNotifications.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/notifications/IdentityNotifications.java @@ -47,8 +47,14 @@ public abstract class IdentityNotifications { public static final String NOTIFICATION_NEW_CONTACT_DEVICE = "identity_manager_notification_new_contact_device"; public static final String NOTIFICATION_NEW_CONTACT_DEVICE_CONTACT_DEVICE_UID_KEY = "contact_device_uid"; public static final String NOTIFICATION_NEW_CONTACT_DEVICE_CONTACT_IDENTITY_KEY = "contact_identity"; + public static final String NOTIFICATION_NEW_CONTACT_DEVICE_CHANNEL_CREATION_ALREADY_IN_PROGRESS_KEY = "channel_creation_already_in_progress"; public static final String NOTIFICATION_NEW_CONTACT_DEVICE_OWNED_IDENTITY_KEY = "owned_identity"; + public static final String NOTIFICATION_NEW_OWNED_DEVICE = "identity_manager_notification_new_owned_device"; + public static final String NOTIFICATION_NEW_OWNED_DEVICE_DEVICE_UID_KEY = "device_uid"; + public static final String NOTIFICATION_NEW_OWNED_DEVICE_CHANNEL_CREATION_ALREADY_IN_PROGRESS_KEY = "channel_creation_already_in_progress"; + public static final String NOTIFICATION_NEW_OWNED_DEVICE_OWNED_IDENTITY_KEY = "owned_identity"; + public static final String NOTIFICATION_GROUP_MEMBER_ADDED = "identity_manager_notification_group_member_added"; public static final String NOTIFICATION_GROUP_MEMBER_ADDED_GROUP_UID_KEY = "group_uid"; public static final String NOTIFICATION_GROUP_MEMBER_ADDED_OWNED_IDENTITY_KEY = "owned_identity"; @@ -78,8 +84,9 @@ public abstract class IdentityNotifications { public static final String NOTIFICATION_PENDING_GROUP_MEMBER_DECLINED_TOGGLED_DECLINED_KEY = "declined"; public static final String NOTIFICATION_GROUP_CREATED = "identity_manager_notification_group_created"; - public static final String NOTIFICATION_GROUP_CREATED_GROUP_OWNER_AND_UID_KEY = "group_uid"; - public static final String NOTIFICATION_GROUP_CREATED_OWNED_IDENTITY_KEY = "owned_identity"; + public static final String NOTIFICATION_GROUP_CREATED_GROUP_OWNER_AND_UID_KEY = "group_uid"; // byte[] + public static final String NOTIFICATION_GROUP_CREATED_OWNED_IDENTITY_KEY = "owned_identity"; // Identity + public static final String NOTIFICATION_GROUP_CREATED_ON_OTHER_DEVICE_KEY = "on_other_device"; // boolean public static final String NOTIFICATION_GROUP_DELETED = "identity_manager_notification_group_deleted"; public static final String NOTIFICATION_GROUP_DELETED_GROUP_OWNER_AND_UID_KEY = "group_uid"; @@ -163,7 +170,8 @@ public abstract class IdentityNotifications { public static final String NOTIFICATION_GROUP_V2_CREATED = "identity_manager_notification_group_v2_created"; public static final String NOTIFICATION_GROUP_V2_CREATED_OWNED_IDENTITY_KEY = "owned_identity"; // Identity public static final String NOTIFICATION_GROUP_V2_CREATED_GROUP_IDENTIFIER_KEY = "group_identifier"; // GroupV2.Identifier - public static final String NOTIFICATION_GROUP_V2_CREATED_NEW_GROUP_KEY = "new_group"; // boolean + public static final String NOTIFICATION_GROUP_V2_CREATED_CREATED_BY_ME_KEY = "created_by_me"; // boolean --> indicates whether the group was created by me (on this device or on another one) + public static final String NOTIFICATION_GROUP_V2_CREATED_ON_OTHER_DEVICE_KEY = "on_other_device"; // boolean --> if NEW_GROUP is true, this tells if the group was created on another owned device public static final String NOTIFICATION_GROUP_V2_DELETED = "identity_manager_notification_group_v2_deleted"; public static final String NOTIFICATION_GROUP_V2_DELETED_OWNED_IDENTITY_KEY = "owned_identity"; // Identity @@ -192,4 +200,7 @@ public abstract class IdentityNotifications { public static final String NOTIFICATION_NEW_KEYCLOAK_GROUP_V2_PUSH_TOPIC = "identity_manager_notification_new_keycloak_group_v2_push_topic"; public static final String NOTIFICATION_NEW_KEYCLOAK_GROUP_V2_PUSH_TOPIC_OWNED_IDENTITY_KEY = "owned_identity"; // Identity + + public static final String NOTIFICATION_OWNED_DEVICE_LIST_CHANGED = "identity_manager_notification_owned_device_list_changed"; + public static final String NOTIFICATION_OWNED_DEVICE_LIST_CHANGED_OWNED_IDENTITY_KEY = "owned_identity"; // Identity } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/notifications/ProtocolNotifications.java b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/notifications/ProtocolNotifications.java index d7e4e9a4..cb8944bc 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/notifications/ProtocolNotifications.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/datatypes/notifications/ProtocolNotifications.java @@ -31,4 +31,20 @@ public abstract class ProtocolNotifications { public static final String NOTIFICATION_GROUP_V2_UPDATE_FAILED_GROUP_IDENTIFIER_KEY = "group_identifier"; // GroupV2.Identifier public static final String NOTIFICATION_GROUP_V2_UPDATE_FAILED_ERROR_KEY = "error"; // boolean: true indicates there was an error, false that there was no change to publish + public static final String NOTIFICATION_OWNED_IDENTITY_DELETED_FROM_ANOTHER_DEVICE = "protocol_manager_notification_owned_identity_deleted_from_another_device"; + public static final String NOTIFICATION_OWNED_IDENTITY_DELETED_FROM_ANOTHER_DEVICE_OWNED_IDENTITY_KEY = "owned_identity"; // Identity + + public static final String NOTIFICATION_KEYCLOAK_SYNCHRONIZATION_REQUIRED = "protocol_manager_notification_keycloak_synchronization_required"; + public static final String NOTIFICATION_KEYCLOAK_SYNCHRONIZATION_REQUIRED_OWNED_IDENTITY_KEY = "owned_identity"; // Identity + + public static final String NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_SENT = "protocol_manager_notification_contact_introduction_invitation_sent"; + public static final String NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_SENT_OWNED_IDENTITY_KEY = "owned_identity"; // Identity + public static final String NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_SENT_CONTACT_IDENTITY_A_KEY = "contact_identity_a"; // Identity + public static final String NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_SENT_CONTACT_IDENTITY_B_KEY = "contact_identity_b"; // Identity + + public static final String NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE = "protocol_manager_notification_contact_introduction_invitation_response"; + public static final String NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE_OWNED_IDENTITY_KEY = "owned_identity"; // Identity + public static final String NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE_MEDIATOR_IDENTITY_KEY = "mediator_identity"; // Identity + public static final String NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE_CONTACT_SERIALIZED_DETAILS_KEY = "contact_serialized_details"; // String + public static final String NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE_ACCEPTED_KEY = "accepted"; // boolean } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/Engine.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/Engine.java index 034a13ba..1dfda224 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/engine/Engine.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/Engine.java @@ -51,12 +51,15 @@ import io.olvid.engine.crypto.AuthEnc; import io.olvid.engine.crypto.Signature; import io.olvid.engine.datatypes.Constants; +import io.olvid.engine.datatypes.DictionaryKey; import io.olvid.engine.datatypes.EncryptedBytes; +import io.olvid.engine.datatypes.OperationQueue; import io.olvid.engine.datatypes.TrustLevel; import io.olvid.engine.datatypes.containers.GroupV2; import io.olvid.engine.datatypes.containers.GroupWithDetails; import io.olvid.engine.datatypes.PushNotificationTypeAndParameters; import io.olvid.engine.datatypes.containers.IdentityWithSerializedDetails; +import io.olvid.engine.datatypes.containers.ServerQuery; import io.olvid.engine.datatypes.containers.TrustOrigin; import io.olvid.engine.datatypes.key.asymmetric.EncryptionEciesMDCKeyPair; import io.olvid.engine.datatypes.key.asymmetric.ServerAuthenticationECSdsaMDCKeyPair; @@ -87,29 +90,38 @@ import io.olvid.engine.engine.types.ObvBackupKeyVerificationOutput; import io.olvid.engine.engine.types.ObvBytesKey; import io.olvid.engine.engine.types.ObvCapability; +import io.olvid.engine.engine.types.ObvDeviceList; +import io.olvid.engine.engine.types.ObvDeviceManagementRequest; import io.olvid.engine.engine.types.ObvDialog; import io.olvid.engine.engine.types.EngineNotificationListener; import io.olvid.engine.engine.types.EngineNotifications; +import io.olvid.engine.engine.types.ObvPushNotificationType; +import io.olvid.engine.engine.types.sync.ObvBackupAndSyncDelegate; +import io.olvid.engine.engine.types.sync.ObvSyncAtom; +import io.olvid.engine.engine.types.RegisterApiKeyResult; import io.olvid.engine.engine.types.identities.ObvContactActiveOrInactiveReason; import io.olvid.engine.engine.types.identities.ObvGroupV2; import io.olvid.engine.engine.types.identities.ObvMutualScanUrl; import io.olvid.engine.engine.types.ObvOutboundAttachment; import io.olvid.engine.engine.types.ObvPostMessageOutput; import io.olvid.engine.engine.types.ObvReturnReceipt; +import io.olvid.engine.engine.types.identities.ObvOwnedDevice; import io.olvid.engine.engine.types.identities.ObvTrustOrigin; import io.olvid.engine.engine.types.identities.ObvGroup; import io.olvid.engine.engine.types.identities.ObvIdentity; import io.olvid.engine.engine.types.identities.ObvKeycloakState; import io.olvid.engine.identity.IdentityManager; import io.olvid.engine.metamanager.CreateSessionDelegate; +import io.olvid.engine.metamanager.EngineOwnedIdentityCleanupDelegate; import io.olvid.engine.metamanager.MetaManager; import io.olvid.engine.networkfetch.FetchManager; import io.olvid.engine.networkfetch.datatypes.DownloadAttachmentPriorityCategory; +import io.olvid.engine.networkfetch.operations.StandaloneServerQueryOperation; import io.olvid.engine.networksend.SendManager; import io.olvid.engine.notification.NotificationManager; import io.olvid.engine.protocol.ProtocolManager; -public class Engine implements UserInterfaceDialogListener, EngineSessionFactory, EngineAPI { +public class Engine implements UserInterfaceDialogListener, EngineSessionFactory, EngineAPI, EngineOwnedIdentityCleanupDelegate { // region fields private long instanceCounter; @@ -137,7 +149,7 @@ public class Engine implements UserInterfaceDialogListener, EngineSessionFactory // endregion - public Engine(File baseDirectory, String dbKey, SSLSocketFactory sslSocketFactory, Logger.LogOutputter logOutputter, int logLevel) throws Exception { + public Engine(File baseDirectory, ObvBackupAndSyncDelegate appBackupAndSyncDelegate, String dbKey, SSLSocketFactory sslSocketFactory, Logger.LogOutputter logOutputter, int logLevel) throws Exception { instanceCounter = 0; listeners = new HashMap<>(); listenersLock = new ReentrantLock(); @@ -212,6 +224,7 @@ public Engine(File baseDirectory, String dbKey, SSLSocketFactory sslSocketFactor MetaManager metaManager = new MetaManager(); this.createSessionDelegate = () -> Session.getSession(dbPath, dbKey); metaManager.registerImplementedDelegates(this.createSessionDelegate); + metaManager.registerImplementedDelegates(this); try (EngineSession engineSession = getSession()) { UserInterfaceDialog.createTable(engineSession.session); @@ -226,7 +239,7 @@ public Engine(File baseDirectory, String dbKey, SSLSocketFactory sslSocketFactor this.fetchManager = new FetchManager(metaManager, sslSocketFactory, baseDirectoryPath, prng, jsonObjectMapper); this.sendManager = new SendManager(metaManager, sslSocketFactory, baseDirectoryPath, prng); this.notificationManager = new NotificationManager(metaManager); - this.protocolManager = new ProtocolManager(metaManager, baseDirectoryPath, prng, jsonObjectMapper); + this.protocolManager = new ProtocolManager(metaManager, appBackupAndSyncDelegate, baseDirectoryPath, prng, jsonObjectMapper); this.backupManager = new BackupManager(metaManager, prng, jsonObjectMapper); registerToInternalNotifications(); @@ -529,6 +542,10 @@ ObvDialog createDialog(ChannelDialogMessageToSend channelDialogMessageToSend) { category = ObvDialog.Category.createGroupV2FrozenInvitation(dialogType.mediatorOrGroupOwnerIdentity.getBytes(), dialogType.obvGroupV2); break; } + case DialogType.SYNC_ITEM_TO_APPLY_DIALOG_ID: { + category = ObvDialog.Category.createSyncItemToApply(dialogType.obvSyncAtom); + break; + } default: Logger.w("Unknown DialogType " + dialogType.id); return null; @@ -538,6 +555,30 @@ ObvDialog createDialog(ChannelDialogMessageToSend channelDialogMessageToSend) { // endregion + // region EngineOwnedIdentityCleanupDelegate + + @Override + public void deleteOwnedIdentityFromInboxOutboxProtocolsAndDialogs(Session session, Identity ownedIdentity, UID excludedProtocolInstanceUid) throws Exception { + protocolManager.deleteOwnedIdentity(session, ownedIdentity, excludedProtocolInstanceUid); + sendManager.deleteOwnedIdentity(session, ownedIdentity); + // do not delete the server session if called with a non-null excludedProtocolInstanceUid + // --> this server session is used in the OwnedIdentityDeletionProtocol to run a server query + fetchManager.deleteOwnedIdentity(session, ownedIdentity, excludedProtocolInstanceUid != null); + + for (UserInterfaceDialog userInterfaceDialog : UserInterfaceDialog.getAll(wrapSession(session))) { + ObvDialog obvDialog = userInterfaceDialog.getObvDialog(); + if (Arrays.equals(obvDialog.getBytesOwnedIdentity(), ownedIdentity.getBytes())) { + userInterfaceDialog.delete(); + } + } + } + + @Override + public void deleteOwnedIdentityServerSession(Session session, Identity ownedIdentity) { + fetchManager.deleteExistingServerSession(session, ownedIdentity, false); + } + // endregion + // region Public API // region Managing Owned Identities @@ -576,15 +617,16 @@ public ObvIdentity getOwnedIdentity(byte[] bytesOwnedIdentity) throws Exception } @Override - public ObvIdentity generateOwnedIdentity(String server, JsonIdentityDetails identityDetails, UUID apiKey, ObvKeycloakState keycloakState) { + public ObvIdentity generateOwnedIdentity(String server, JsonIdentityDetails identityDetails, ObvKeycloakState keycloakState, String deviceDisplayName) { try (EngineSession engineSession = getSession()) { if (server == null) { server = ""; } - Identity identity = identityManager.generateOwnedIdentity(engineSession.session, server, identityDetails, apiKey, keycloakState, prng); + Identity identity = identityManager.generateOwnedIdentity(engineSession.session, server, identityDetails, keycloakState, deviceDisplayName, prng); if (identity == null) { return null; } + ObvIdentity ownedIdentity = new ObvIdentity(identity, identityDetails, keycloakState != null, true); engineSession.session.commit(); return ownedIdentity; @@ -595,26 +637,47 @@ public ObvIdentity generateOwnedIdentity(String server, JsonIdentityDetails iden @Override - public UUID getApiKeyForOwnedIdentity(byte[] bytesOwnedIdentity) { + public RegisterApiKeyResult registerOwnedIdentityApiKeyOnServer(byte[] bytesOwnedIdentity, UUID apiKey) { try { Identity ownedIdentity = Identity.of(bytesOwnedIdentity); - return identityManager.getApiKey(ownedIdentity); - } catch (Exception e) { - return null; - } - } + byte[] serverSessionToken = fetchManager.getServerAuthenticationToken(ownedIdentity); + if (serverSessionToken == null) { + fetchManager.createServerSession(ownedIdentity); + return RegisterApiKeyResult.FAILED; + } - @Override - public boolean updateApiKeyForOwnedIdentity(byte[] bytesOwnedIdentity, UUID apiKey) { - try (EngineSession engineSession = getSession()) { - Identity ownedIdentity = Identity.of(bytesOwnedIdentity); - identityManager.updateApiKeyOfOwnedIdentity(engineSession.session, ownedIdentity, apiKey); - fetchManager.deleteExistingServerSessionAndCreateANewOne(engineSession.session, ownedIdentity); - engineSession.session.commit(); - return true; + StandaloneServerQueryOperation standaloneServerQueryOperation = new StandaloneServerQueryOperation(new ServerQuery(null, ownedIdentity, ServerQuery.Type.createRegisterApiKey(ownedIdentity, serverSessionToken, Logger.getUuidString(apiKey)))); + + OperationQueue queue = new OperationQueue(); + queue.queue(standaloneServerQueryOperation); + queue.execute(1, "Engine-registerOwnedIdentityApiKeyOnServer"); + queue.join(); + + if (standaloneServerQueryOperation.isFinished()) { + recreateServerSession(bytesOwnedIdentity); + return RegisterApiKeyResult.SUCCESS; + } else { + if (standaloneServerQueryOperation.getReasonForCancel() != null) { + switch (standaloneServerQueryOperation.getReasonForCancel()) { + case StandaloneServerQueryOperation.RFC_INVALID_API_KEY: { + return RegisterApiKeyResult.INVALID_KEY; + } + case StandaloneServerQueryOperation.RFC_INVALID_SERVER_SESSION: { + recreateServerSession(bytesOwnedIdentity); + break; + } + case StandaloneServerQueryOperation.RFC_UNSUPPORTED_SERVER_QUERY_TYPE: + case StandaloneServerQueryOperation.RFC_NETWORK_ERROR: + default: { + break; + } + } + } + return RegisterApiKeyResult.FAILED; + } } catch (Exception e) { e.printStackTrace(); - return false; + return RegisterApiKeyResult.FAILED; } } @@ -701,7 +764,7 @@ public boolean updateKeycloakGroups(byte[] bytesOwnedIdentity, List sign public void recreateServerSession(byte[] bytesOwnedIdentity) { try (EngineSession engineSession = getSession()) { Identity ownedIdentity = Identity.of(bytesOwnedIdentity); - fetchManager.deleteExistingServerSessionAndCreateANewOne(engineSession.session, ownedIdentity); + fetchManager.deleteExistingServerSession(engineSession.session, ownedIdentity, true); engineSession.session.commit(); } catch (Exception e) { e.printStackTrace(); @@ -715,20 +778,14 @@ public void deleteOwnedIdentity(byte[] bytesOwnedIdentity) throws Exception { engineSession.session.startTransaction(); channelManager.deleteAllChannelsForOwnedIdentity(engineSession.session, ownedIdentity); identityManager.deleteOwnedIdentity(engineSession.session, ownedIdentity); - protocolManager.deleteOwnedIdentity(engineSession.session, ownedIdentity); - sendManager.deleteOwnedIdentity(engineSession.session, ownedIdentity); - fetchManager.deleteOwnedIdentity(engineSession.session, ownedIdentity); - - for (UserInterfaceDialog userInterfaceDialog: UserInterfaceDialog.getAll(engineSession)) { - ObvDialog obvDialog = userInterfaceDialog.getObvDialog(); - if (Arrays.equals(obvDialog.getBytesOwnedIdentity(), bytesOwnedIdentity)) { - userInterfaceDialog.delete(); - } - } + + deleteOwnedIdentityFromInboxOutboxProtocolsAndDialogs(engineSession.session, ownedIdentity, null); + engineSession.session.commit(); } } + @Override public JsonIdentityDetailsWithVersionAndPhoto[] getOwnedIdentityPublishedAndLatestDetails(byte[] bytesOwnedIdentity) throws Exception { try (EngineSession engineSession = getSession()) { @@ -765,6 +822,14 @@ public void saveKeycloakJwks(byte[] bytesOwnedIdentity, String serializedJwks) t } } + @Override + public void saveKeycloakApiKey(byte[] bytesOwnedIdentity, String apiKey) throws Exception { + try (EngineSession engineSession = getSession()) { + Identity ownedIdentity = Identity.of(bytesOwnedIdentity); + identityManager.saveKeycloakApiKey(engineSession.session, ownedIdentity, apiKey); + engineSession.session.commit(); + } + } @Override public Collection getOwnedIdentitiesWithKeycloakPushTopic(String pushTopic) throws Exception { @@ -835,19 +900,41 @@ public void unbindOwnedIdentityFromKeycloak(byte[] bytesOwnedIdentity) { } @Override - public void registerToPushNotification(byte[] bytesOwnedIdentity, String firebaseToken, boolean kickOtherDevices, boolean useMultidevice) throws Exception { + public void registerToPushNotification(byte[] bytesOwnedIdentity, ObvPushNotificationType pushNotificationType, boolean reactivateCurrentDevice, byte[] bytesDeviceUidToReplace) throws Exception { try (EngineSession engineSession = getSession()) { Identity ownedIdentity = Identity.of(bytesOwnedIdentity); UID currentDeviceUid = identityManager.getCurrentDeviceUidOfOwnedIdentity(engineSession.session, ownedIdentity); + UID deviceUidToReplace = bytesDeviceUidToReplace == null ? null : new UID(bytesDeviceUidToReplace); PushNotificationTypeAndParameters pushNotificationTypeAndParameters; - if (firebaseToken == null) { - pushNotificationTypeAndParameters = PushNotificationTypeAndParameters.createWebsocketOnly(kickOtherDevices, useMultidevice); - } else { - // We pick a random identityMaskingUid in case we need to register (only useful when configuration changed) - UID identityMaskingUid = new UID(prng); - byte[] firebaseTokenBytes = firebaseToken.getBytes(StandardCharsets.UTF_8); - pushNotificationTypeAndParameters = PushNotificationTypeAndParameters.createFirebaseAndroid(firebaseTokenBytes, identityMaskingUid, kickOtherDevices, useMultidevice); + switch (pushNotificationType.platform) { + case ANDROID: { + if (pushNotificationType.firebaseToken == null) { + pushNotificationTypeAndParameters = PushNotificationTypeAndParameters.createWebsocketOnlyAndroid(reactivateCurrentDevice, deviceUidToReplace); + } else { + // We pick a random identityMaskingUid in case we need to register (only useful when configuration changed) + UID identityMaskingUid = new UID(prng); + byte[] firebaseTokenBytes = pushNotificationType.firebaseToken.getBytes(StandardCharsets.UTF_8); + pushNotificationTypeAndParameters = PushNotificationTypeAndParameters.createFirebaseAndroid(firebaseTokenBytes, identityMaskingUid, reactivateCurrentDevice, deviceUidToReplace); + } + break; + } + case WINDOWS: { + pushNotificationTypeAndParameters = PushNotificationTypeAndParameters.createWindows(reactivateCurrentDevice, deviceUidToReplace); + break; + } + case LINUX: { + pushNotificationTypeAndParameters = PushNotificationTypeAndParameters.createLinux(reactivateCurrentDevice, deviceUidToReplace); + break; + } + case DAEMON: { + pushNotificationTypeAndParameters = PushNotificationTypeAndParameters.createDaemon(reactivateCurrentDevice, deviceUidToReplace); + break; + } + default: { + Logger.e("Engine.registerToPushNotification: unknown pushNotificationType.platform"); + throw new Exception(); + } } engineSession.session.startTransaction(); fetchManager.registerPushNotificationIfConfigurationChanged(engineSession.session, ownedIdentity, currentDeviceUid, pushNotificationTypeAndParameters); @@ -856,15 +943,6 @@ public void registerToPushNotification(byte[] bytesOwnedIdentity, String firebas } - @Override - public void unregisterToPushNotification(byte[] bytesOwnedIdentity) throws Exception { - try (EngineSession engineSession = getSession()) { - Identity ownedIdentity = Identity.of(bytesOwnedIdentity); - fetchManager.unregisterPushNotification(engineSession.session, ownedIdentity); - engineSession.session.commit(); - } - } - @Override public void processAndroidPushNotification(String maskingUidString) { fetchManager.processAndroidPushNotification(maskingUidString); @@ -879,6 +957,15 @@ public byte[] getOwnedIdentityFromMaskingUid(String maskingUidString) { return null; } + @Override + public void processDeviceManagementRequest(byte[] bytesOwnedIdentity, ObvDeviceManagementRequest deviceManagementRequest) throws Exception { + Identity ownedIdentity = Identity.of(bytesOwnedIdentity); + try (EngineSession engineSession = getSession()) { + protocolManager.processDeviceManagementRequest(engineSession.session, ownedIdentity, deviceManagementRequest); + engineSession.session.commit(); + } + } + @Override public void updateLatestIdentityDetails(byte[] bytesOwnedIdentity, JsonIdentityDetails jsonIdentityDetails) throws Exception { Identity ownedIdentity = Identity.of(bytesOwnedIdentity); @@ -949,6 +1036,115 @@ public List getOwnCapabilities(byte[] bytesOwnedIdentity) { } } + @Override + public List getOwnedDevices(byte[] bytesOwnedIdentity) { + try (EngineSession engineSession = getSession()) { + Identity ownedIdentity = Identity.of(bytesOwnedIdentity); + return identityManager.getDevicesOfOwnedIdentity(engineSession.session, ownedIdentity); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + @Override + public ObvDeviceList queryRegisteredOwnedDevicesFromServer(byte[] bytesOwnedIdentity) { + try (EngineSession engineSession = getSession()) { + Identity ownedIdentity = Identity.of(bytesOwnedIdentity); + + StandaloneServerQueryOperation standaloneServerQueryOperation = new StandaloneServerQueryOperation(new ServerQuery(null, ownedIdentity, ServerQuery.Type.createOwnedDeviceDiscoveryQuery(ownedIdentity))); + + OperationQueue queue = new OperationQueue(); + queue.queue(standaloneServerQueryOperation); + queue.execute(1, "Engine-queryRegisterOwnedDevicesFromServer"); + queue.join(); + + if (standaloneServerQueryOperation.isFinished() && standaloneServerQueryOperation.getServerResponse() != null) { + // decrypt the received device list + byte[] decryptedPayload = identityManager.decrypt(engineSession.session, standaloneServerQueryOperation.getServerResponse().decodeEncryptedData(), ownedIdentity); + + HashMap map = new Encoded(decryptedPayload).decodeDictionary(); + + // check for multi-device (is null if server could not determine if multi-device is available) + Encoded encodedMulti = map.get(new DictionaryKey("multi")); + Boolean multiDevice; + if (encodedMulti != null) { + multiDevice = encodedMulti.decodeBoolean(); + } else { + multiDevice = null; + } + + // now get the actual device list + HashMap deviceUidsAndServerInfo = new HashMap<>(); + + Encoded[] encodedDevices = map.get(new DictionaryKey("dev")).decodeList(); + for (Encoded encodedDevice : encodedDevices) { + HashMap deviceMap = encodedDevice.decodeDictionary(); + UID deviceUid = deviceMap.get(new DictionaryKey("uid")).decodeUid(); + + Encoded encodedExpiration = deviceMap.get(new DictionaryKey("exp")); + Long expirationTimestamp = encodedExpiration == null ? null : encodedExpiration.decodeLong(); + + Encoded encodedRegistration = deviceMap.get(new DictionaryKey("reg")); + Long lastRegistrationTimestamp = encodedRegistration == null ? null : encodedRegistration.decodeLong(); + + Encoded encodedName = deviceMap.get(new DictionaryKey("name")); + String deviceName = null; + if (encodedName != null) { + try { + byte[] plaintext = identityManager.decrypt(engineSession.session, encodedName.decodeEncryptedData(), ownedIdentity); + byte[] bytesDeviceName = new Encoded(plaintext).decodeListWithPadding()[0].decodeBytes(); + if (bytesDeviceName.length != 0) { + deviceName = new String(bytesDeviceName, StandardCharsets.UTF_8); + } + } catch (Exception ignored) { + } + } + + deviceUidsAndServerInfo.put(new ObvBytesKey(deviceUid.getBytes()), new ObvOwnedDevice.ServerDeviceInfo(deviceName, expirationTimestamp, lastRegistrationTimestamp)); + } + + return new ObvDeviceList(multiDevice, deviceUidsAndServerInfo); + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + @Override + public void refreshOwnedDeviceList(byte[] bytesOwnedIdentity) { + try { + Identity ownedIdentity = Identity.of(bytesOwnedIdentity); + protocolManager.startOwnedDeviceDiscoveryProtocol(ownedIdentity); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void recreateOwnedDeviceChannel(byte[] bytesOwnedIdentity, byte[] bytesDeviceUid) { + try { + Identity ownedIdentity = Identity.of(bytesOwnedIdentity); + UID deviceUid = new UID(bytesDeviceUid); + // simply start the channel creation protocol: this deletes any channel and aborts any ongoing instance + protocolManager.startChannelCreationWithOwnedDeviceProtocol(ownedIdentity, deviceUid); + } catch (Exception e) { + e.printStackTrace(); + } + } + +// @Override +// public void resynchronizeAllOwnedDevices(byte[] bytesOwnedIdentity) { +// try (EngineSession engineSession = getSession()) { +// Identity ownedIdentity = Identity.of(bytesOwnedIdentity); +// protocolManager.triggerOwnedDevicesSync(engineSession.session, ownedIdentity); +// engineSession.session.commit(); +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } + // endregion // region Managing Contact Identities @@ -1058,7 +1254,10 @@ public void trustPublishedContactDetails(byte[] bytesOwnedIdentity, byte[] bytes try (EngineSession engineSession = getSession()) { Identity contactIdentity = Identity.of(bytesContactIdentity); Identity ownedIdentity = Identity.of(bytesOwnedIdentity); - identityManager.trustPublishedContactDetails(engineSession.session, contactIdentity, ownedIdentity); + JsonIdentityDetailsWithVersionAndPhoto details = identityManager.trustPublishedContactDetails(engineSession.session, contactIdentity, ownedIdentity); + if (details != null) { + propagateEngineSyncAtomToOtherDevicesIfNeeded(engineSession.session, ownedIdentity, ObvSyncAtom.createTrustContactDetails(contactIdentity, jsonObjectMapper.writeValueAsString(details))); + } engineSession.session.commit(); } catch (Exception e) { e.printStackTrace(); @@ -1184,7 +1383,10 @@ public void trustPublishedGroupDetails(byte[] bytesOwnedIdentity, byte[] bytesGr try (EngineSession engineSession = getSession()) { engineSession.session.startTransaction(); Identity ownedIdentity = Identity.of(bytesOwnedIdentity); - identityManager.trustPublishedGroupDetails(engineSession.session, ownedIdentity, bytesGroupOwnerAndUid); + JsonGroupDetailsWithVersionAndPhoto details = identityManager.trustPublishedGroupDetails(engineSession.session, ownedIdentity, bytesGroupOwnerAndUid); + if (details != null) { + propagateEngineSyncAtomToOtherDevicesIfNeeded(engineSession.session, ownedIdentity, ObvSyncAtom.createTrustGroupV1Details(bytesGroupOwnerAndUid, jsonObjectMapper.writeValueAsString(details))); + } engineSession.session.commit(); } catch (Exception e) { e.printStackTrace(); @@ -1255,11 +1457,33 @@ public void trustGroupV2PublishedDetails(byte[] bytesOwnedIdentity, byte[] bytes Identity ownedIdentity = Identity.of(bytesOwnedIdentity); GroupV2.Identifier groupIdentifier = GroupV2.Identifier.of(bytesGroupIdentifier); try (EngineSession engineSession = getSession()) { - identityManager.trustGroupV2PublishedDetails(engineSession.session, ownedIdentity, groupIdentifier); + int version = identityManager.trustGroupV2PublishedDetails(engineSession.session, ownedIdentity, groupIdentifier); + if (version != -1) { + propagateEngineSyncAtomToOtherDevicesIfNeeded(engineSession.session, ownedIdentity, ObvSyncAtom.createTrustGroupV2Details(groupIdentifier, version)); + } engineSession.session.commit(); } } + @Override + public String getGroupV2JsonType(byte[] bytesOwnedIdentity, byte[] bytesGroupIdentifier) { + if (bytesOwnedIdentity == null || bytesGroupIdentifier == null) { + return null; + } + + try { + Identity ownedIdentity = Identity.of(bytesOwnedIdentity); + GroupV2.Identifier groupIdentifier = GroupV2.Identifier.of(bytesGroupIdentifier); + + try (EngineSession engineSession = getSession()) { + return identityManager.getGroupV2JsonGroupType(engineSession.session, ownedIdentity, groupIdentifier); + } + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + @Override public ObvGroupV2.ObvGroupV2DetailsAndPhotos getGroupV2DetailsAndPhotos(byte[] bytesOwnedIdentity, byte[] bytesGroupIdentifier) { if (bytesOwnedIdentity == null || bytesGroupIdentifier == null) { @@ -1299,6 +1523,10 @@ public void leaveGroupV2(byte[] bytesOwnedIdentity, byte[] bytesGroupIdentifier) } Identity ownedIdentity = Identity.of(bytesOwnedIdentity); GroupV2.Identifier groupIdentifier = GroupV2.Identifier.of(bytesGroupIdentifier); + if (groupIdentifier.category == GroupV2.Identifier.CATEGORY_KEYCLOAK) { + // it is not possible to leave a keycloak group + return; + } protocolManager.initiateGroupV2Leave(ownedIdentity, groupIdentifier); } @@ -1310,6 +1538,10 @@ public void disbandGroupV2(byte[] bytesOwnedIdentity, byte[] bytesGroupIdentifie } Identity ownedIdentity = Identity.of(bytesOwnedIdentity); GroupV2.Identifier groupIdentifier = GroupV2.Identifier.of(bytesGroupIdentifier); + if (groupIdentifier.category == GroupV2.Identifier.CATEGORY_KEYCLOAK) { + // it is not possible to leave a keycloak group + return; + } protocolManager.initiateGroupV2Disband(ownedIdentity, groupIdentifier); } @@ -1493,7 +1725,7 @@ public void startGroupCreationProtocol(String serializedGroupDetailsWithVersionA @Override - public void startGroupV2CreationProtocol(String serializedGroupDetails, String absolutePhotoUrl, byte[] bytesOwnedIdentity, HashSet ownPermissions, HashMap> otherGroupMembers) throws Exception { + public void startGroupV2CreationProtocol(String serializedGroupDetails, String absolutePhotoUrl, byte[] bytesOwnedIdentity, HashSet ownPermissions, HashMap> otherGroupMembers, String serializedGroupType) throws Exception { Identity ownedIdentity = Identity.of(bytesOwnedIdentity); HashSet otherGroupMembersSet = new HashSet<>(); @@ -1502,7 +1734,7 @@ public void startGroupV2CreationProtocol(String serializedGroupDetails, String a otherGroupMembersSet.add(new GroupV2.IdentityAndPermissions(remoteIdentity, entry.getValue())); } - protocolManager.startGroupV2CreationProtocol(ownedIdentity, serializedGroupDetails, absolutePhotoUrl, ownPermissions, otherGroupMembersSet); + protocolManager.startGroupV2CreationProtocol(ownedIdentity, serializedGroupDetails, absolutePhotoUrl, ownPermissions, otherGroupMembersSet, serializedGroupType); } @Override @@ -1599,26 +1831,13 @@ public void startOneToOneInvitationProtocol(byte[] bytesOwnedIdentity, byte[] by } @Override - public void deleteOwnedIdentityAndNotifyContacts(byte[] bytesOwnedIdentity) throws Exception { + public void deleteOwnedIdentityAndNotifyContacts(byte[] bytesOwnedIdentity, boolean deleteEverywhere) throws Exception { try (EngineSession engineSession = getSession()) { Identity ownedIdentity = Identity.of(bytesOwnedIdentity); - engineSession.session.startTransaction(); - - // before starting the protocol in charge of notifying the contacts, delete everything that will no longer be used - protocolManager.deleteOwnedIdentity(engineSession.session, ownedIdentity); - sendManager.deleteOwnedIdentity(engineSession.session, ownedIdentity); - fetchManager.deleteOwnedIdentity(engineSession.session, ownedIdentity); - - for (UserInterfaceDialog userInterfaceDialog: UserInterfaceDialog.getAll(engineSession)) { - ObvDialog obvDialog = userInterfaceDialog.getObvDialog(); - if (Arrays.equals(obvDialog.getBytesOwnedIdentity(), bytesOwnedIdentity)) { - userInterfaceDialog.delete(); - } - } // now delete contacts and leave/disband groups // the protocol will also delete all channels (once they are no longer used) and actually delete the owned identity - protocolManager.deleteOwnedIdentityAndNotifyContacts(engineSession.session, ownedIdentity); + protocolManager.startOwnedIdentityDeletionProtocol(engineSession.session, ownedIdentity, deleteEverywhere); engineSession.session.commit(); } } @@ -1789,9 +2008,15 @@ public void sendReturnReceipt(byte[] bytesOwnedIdentity, byte[] bytesContactIden Identity contactIdentity = Identity.of(bytesContactIdentity); AuthEncKey returnReceiptKey = (AuthEncKey) new Encoded(returnReceiptKeyBytes).decodeSymmetricKey(); // fetch contact deviceUids - UID[] contactDeviceUids = identityManager.getDeviceUidsOfContactIdentity(engineSession.session, ownedIdentity, contactIdentity); - if (contactDeviceUids.length != 0) { - sendManager.sendReturnReceipt(engineSession.session, ownedIdentity, contactIdentity, contactDeviceUids, status, returnReceiptNonce, returnReceiptKey, attachmentNumber); + final UID[] deviceUids; + // To improve: maybe find a way to send the return receipt only to the device that actually sent the message? + if (Arrays.equals(bytesOwnedIdentity, bytesContactIdentity)) { + deviceUids = identityManager.getOtherDeviceUidsOfOwnedIdentity(engineSession.session, ownedIdentity); + } else { + deviceUids = identityManager.getDeviceUidsOfContactIdentity(engineSession.session, ownedIdentity, contactIdentity); + } + if (deviceUids.length != 0) { + sendManager.sendReturnReceipt(engineSession.session, ownedIdentity, contactIdentity, deviceUids, status, returnReceiptNonce, returnReceiptKey, attachmentNumber); } engineSession.session.commit(); } catch (Exception e) { @@ -1965,8 +2190,8 @@ public void resendAllAttachmentNotifications() throws Exception { } @Override - public void connectWebsocket(String os, String osVersion, int appBuild, String appVersion) { - fetchManager.connectWebsockets(os, osVersion, appBuild, appVersion); + public void connectWebsocket(boolean relyOnWebsocketForNetworkDetection, String os, String osVersion, int appBuild, String appVersion) { + fetchManager.connectWebsockets(relyOnWebsocketForNetworkDetection, os, osVersion, appBuild, appVersion); } @Override @@ -2059,21 +2284,13 @@ public ObvBackupKeyVerificationOutput verifyBackupSeed(String backupSeedString) } @Override - public ObvIdentity[] restoreOwnedIdentitiesFromBackup(String backupSeed, byte[] backupContent) { - return backupManager.restoreOwnedIdentitiesFromBackup(backupSeed, backupContent); + public ObvIdentity[] restoreOwnedIdentitiesFromBackup(String backupSeed, byte[] backupContent, String deviceDisplayName) { + return backupManager.restoreOwnedIdentitiesFromBackup(backupSeed, backupContent, deviceDisplayName); } @Override public void restoreContactsAndGroupsFromBackup(String backupSeed, byte[] backupContent, ObvIdentity[] restoredOwnedIdentities) { - Identity[] restoredIdentities = new Identity[restoredOwnedIdentities.length]; - for (int i=0; i 0) { + protocolManager.initiateSingleItemSync(engineSession.session, ownedIdentity, obvSyncAtom); + } + } + engineSession.session.commit(); + } + } + + @Override + public void propagateAppSyncAtomToOtherDevicesIfNeeded(byte[] bytesOwnedIdentity, ObvSyncAtom obvSyncAtom) throws Exception { + // the App should never be sending a non-app sync item + if (!obvSyncAtom.isAppSyncItem()) { + throw new Exception(); + } + + try (EngineSession engineSession = getSession()) { + Identity ownedIdentity = Identity.of(bytesOwnedIdentity); + + if (identityManager.getOtherDeviceUidsOfOwnedIdentity(engineSession.session, ownedIdentity).length > 0) { + protocolManager.initiateSingleItemSync(engineSession.session, ownedIdentity, obvSyncAtom); + engineSession.session.commit(); + } + } + } + + private boolean propagateEngineSyncAtomToOtherDevicesIfNeeded(Session session, Identity ownedIdentity, ObvSyncAtom obvSyncAtom) throws Exception { + // the App should never be sending a non-app sync item + if (obvSyncAtom.isAppSyncItem()) { + throw new Exception(); + } + + if (identityManager.getOtherDeviceUidsOfOwnedIdentity(session, ownedIdentity).length > 0) { + protocolManager.initiateSingleItemSync(session, ownedIdentity, obvSyncAtom); + return true; + } + return false; + } + + // Run once after you upgrade from a version not handling Contact and ContactGroup UserData to a version able to do so // Also run after a backup restore @Override @@ -2199,6 +2465,20 @@ public void downloadAllUserData() throws Exception { } } + // Run once after the first introduction of device names for multi-device + @Override + public void setAllOwnedDeviceNames(String deviceName) { + try (EngineSession engineSession = getSession()) { + for (Identity ownedIdentity : identityManager.getOwnedIdentities(engineSession.session)) { + UID currentDeviceUid = identityManager.getCurrentDeviceUidOfOwnedIdentity(engineSession.session, ownedIdentity); + protocolManager.processDeviceManagementRequest(engineSession.session, ownedIdentity, ObvDeviceManagementRequest.createSetNicknameRequest(currentDeviceUid.getBytes(), deviceName)); + } + engineSession.session.commit(); + } catch (Exception e) { + e.printStackTrace(); + } + } + @Override public void vacuumDatabase() throws Exception { try (EngineSession engineSession = getSession()) { diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerChannelsAndProtocols.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerChannelsAndProtocols.java index 2dadc5e7..f38843a4 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerChannelsAndProtocols.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerChannelsAndProtocols.java @@ -50,6 +50,10 @@ void registerToNotifications(NotificationManager notificationManager) { ChannelNotifications.NOTIFICATION_OBLIVIOUS_CHANNEL_DELETED, ProtocolNotifications.NOTIFICATION_MUTUAL_SCAN_CONTACT_ADDED, ProtocolNotifications.NOTIFICATION_GROUP_V2_UPDATE_FAILED, + ProtocolNotifications.NOTIFICATION_OWNED_IDENTITY_DELETED_FROM_ANOTHER_DEVICE, + ProtocolNotifications.NOTIFICATION_KEYCLOAK_SYNCHRONIZATION_REQUIRED, + ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_SENT, + ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE, }) { notificationManager.addListener(notificationName, this); } @@ -88,7 +92,7 @@ public void callback(String notificationName, HashMap userInfo) } HashMap engineInfo = new HashMap<>(); - Identity ownedIdentity = engine.identityManager.getOwnedIdentityForDeviceUid(engineSession.session, currentDeviceUid); + Identity ownedIdentity = engine.identityManager.getOwnedIdentityForCurrentDeviceUid(engineSession.session, currentDeviceUid); engineInfo.put(EngineNotifications.CHANNEL_CONFIRMED_OR_DELETED_OWNED_IDENTITY_KEY, ownedIdentity.getBytes()); engineInfo.put(EngineNotifications.CHANNEL_CONFIRMED_OR_DELETED_CONTACT_IDENTITY_KEY, contactIdentity.getBytes()); @@ -107,7 +111,7 @@ public void callback(String notificationName, HashMap userInfo) } HashMap engineInfo = new HashMap<>(); - Identity ownedIdentity = engine.identityManager.getOwnedIdentityForDeviceUid(engineSession.session, currentDeviceUid); + Identity ownedIdentity = engine.identityManager.getOwnedIdentityForCurrentDeviceUid(engineSession.session, currentDeviceUid); if (ownedIdentity == null) { break; } @@ -154,6 +158,64 @@ public void callback(String notificationName, HashMap userInfo) engine.postEngineNotification(EngineNotifications.GROUP_V2_UPDATE_FAILED, engineInfo); break; } + case ProtocolNotifications.NOTIFICATION_OWNED_IDENTITY_DELETED_FROM_ANOTHER_DEVICE: { + Identity ownedIdentity = (Identity) userInfo.get(ProtocolNotifications.NOTIFICATION_OWNED_IDENTITY_DELETED_FROM_ANOTHER_DEVICE_OWNED_IDENTITY_KEY); + if (ownedIdentity == null) { + break; + } + HashMap engineInfo = new HashMap<>(); + engineInfo.put(EngineNotifications.OWNED_IDENTITY_DELETED_FROM_ANOTHER_DEVICE_BYTES_OWNED_IDENTITY_KEY, ownedIdentity.getBytes()); + + engine.postEngineNotification(EngineNotifications.OWNED_IDENTITY_DELETED_FROM_ANOTHER_DEVICE, engineInfo); + break; + } + case ProtocolNotifications.NOTIFICATION_KEYCLOAK_SYNCHRONIZATION_REQUIRED: { + Identity ownedIdentity = (Identity) userInfo.get(ProtocolNotifications.NOTIFICATION_KEYCLOAK_SYNCHRONIZATION_REQUIRED_OWNED_IDENTITY_KEY); + if (ownedIdentity == null) { + break; + } + HashMap engineInfo = new HashMap<>(); + engineInfo.put(EngineNotifications.KEYCLOAK_SYNCHRONIZATION_REQUIRED_BYTES_OWNED_IDENTITY_KEY, ownedIdentity.getBytes()); + + engine.postEngineNotification(EngineNotifications.KEYCLOAK_SYNCHRONIZATION_REQUIRED, engineInfo); + break; + } + case ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_SENT: { + Identity ownedIdentity = (Identity) userInfo.get(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_SENT_OWNED_IDENTITY_KEY); + Identity contactIdentityA = (Identity) userInfo.get(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_SENT_CONTACT_IDENTITY_A_KEY); + Identity contactIdentityB = (Identity) userInfo.get(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_SENT_CONTACT_IDENTITY_B_KEY); + + if (ownedIdentity == null || contactIdentityA == null || contactIdentityB == null) { + break; + } + + HashMap engineInfo = new HashMap<>(); + engineInfo.put(EngineNotifications.CONTACT_INTRODUCTION_INVITATION_SENT_BYTES_OWNED_IDENTITY_KEY, ownedIdentity.getBytes()); + engineInfo.put(EngineNotifications.CONTACT_INTRODUCTION_INVITATION_SENT_BYTES_CONTACT_IDENTITY_A_KEY, contactIdentityA.getBytes()); + engineInfo.put(EngineNotifications.CONTACT_INTRODUCTION_INVITATION_SENT_BYTES_CONTACT_IDENTITY_B_KEY, contactIdentityB.getBytes()); + + engine.postEngineNotification(EngineNotifications.CONTACT_INTRODUCTION_INVITATION_SENT, engineInfo); + break; + } + case ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE: { + Identity ownedIdentity = (Identity) userInfo.get(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE_OWNED_IDENTITY_KEY); + Identity mediatorIdentity = (Identity) userInfo.get(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE_MEDIATOR_IDENTITY_KEY); + String contactSerializedDetails = (String) userInfo.get(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE_CONTACT_SERIALIZED_DETAILS_KEY); + Boolean accepted = (Boolean) userInfo.get(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE_ACCEPTED_KEY); + + if (ownedIdentity == null || mediatorIdentity == null || contactSerializedDetails == null || accepted == null) { + break; + } + + HashMap engineInfo = new HashMap<>(); + engineInfo.put(EngineNotifications.CONTACT_INTRODUCTION_INVITATION_RESPONSE_BYTES_OWNED_IDENTITY_KEY, ownedIdentity.getBytes()); + engineInfo.put(EngineNotifications.CONTACT_INTRODUCTION_INVITATION_RESPONSE_BYTES_MEDIATOR_IDENTITY_KEY, mediatorIdentity.getBytes()); + engineInfo.put(EngineNotifications.CONTACT_INTRODUCTION_INVITATION_RESPONSE_CONTACT_SERIALIZED_DETAILS_KEY, contactSerializedDetails); + engineInfo.put(EngineNotifications.CONTACT_INTRODUCTION_INVITATION_RESPONSE_ACCEPTED_KEY, accepted); + + engine.postEngineNotification(EngineNotifications.CONTACT_INTRODUCTION_INVITATION_RESPONSE, engineInfo); + break; + } default: Logger.w("Received notification " + notificationName + " but no handler is set."); } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerDownloads.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerDownloads.java index 99e27b9e..bbbb6169 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerDownloads.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerDownloads.java @@ -40,6 +40,7 @@ public class NotificationListenerDownloads implements NotificationListener { private final Engine engine; + private long latestNetworkRestart = System.currentTimeMillis(); public NotificationListenerDownloads(Engine engine) { this.engine = engine; @@ -72,8 +73,10 @@ void registerToNotifications(NotificationManager notificationManager) { DownloadNotifications.NOTIFICATION_PING_LOST, DownloadNotifications.NOTIFICATION_PING_RECEIVED, DownloadNotifications.NOTIFICATION_WEBSOCKET_CONNECTION_STATE_CHANGED, + DownloadNotifications.NOTIFICATION_WEBSOCKET_DETECTED_SOME_NETWORK, DownloadNotifications.NOTIFICATION_PUSH_TOPIC_NOTIFIED, DownloadNotifications.NOTIFICATION_PUSH_KEYCLOAK_UPDATE_REQUIRED, + DownloadNotifications.NOTIFICATION_PUSH_REGISTER_FAILED_BAD_DEVICE_UID_TO_REPLACE, }) { notificationManager.addListener(notificationName, this); } @@ -209,6 +212,9 @@ public void callback(String notificationName, HashMap userInfo) case WEB_CLIENT: enginePermissions.add(EngineAPI.ApiKeyPermission.WEB_CLIENT); break; + case MULTI_DEVICE: + enginePermissions.add(EngineAPI.ApiKeyPermission.MULTI_DEVICE); + break; } } engineInfo.put(EngineNotifications.API_KEY_ACCEPTED_PERMISSIONS_KEY, enginePermissions); @@ -345,6 +351,9 @@ public void callback(String notificationName, HashMap userInfo) case WEB_CLIENT: enginePermissions.add(EngineAPI.ApiKeyPermission.WEB_CLIENT); break; + case MULTI_DEVICE: + enginePermissions.add(EngineAPI.ApiKeyPermission.MULTI_DEVICE); + break; } } engineInfo.put(EngineNotifications.API_KEY_STATUS_QUERY_SUCCESS_PERMISSIONS_KEY, enginePermissions); @@ -487,6 +496,16 @@ public void callback(String notificationName, HashMap userInfo) engine.postEngineNotification(EngineNotifications.WEBSOCKET_CONNECTION_STATE_CHANGED, engineInfo); break; } + case DownloadNotifications.NOTIFICATION_WEBSOCKET_DETECTED_SOME_NETWORK: { + // this notification is only sent if websocket are monitoring network state. In case network is detected --> reschedule all network tasks + if (latestNetworkRestart + 5_000 < System.currentTimeMillis()) { + latestNetworkRestart = System.currentTimeMillis(); + Logger.i("Network detected (WebSocket connected), retrying all scheduled network jobs"); + engine.retryScheduledNetworkTasks(); + } + engine.postEngineNotification(EngineNotifications.WEBSOCKET_DETECTED_SOME_NETWORK, new HashMap<>()); + break; + } case DownloadNotifications.NOTIFICATION_PUSH_TOPIC_NOTIFIED: { String topic = (String) userInfo.get(DownloadNotifications.NOTIFICATION_PUSH_TOPIC_NOTIFIED_TOPIC_KEY); if (topic == null) { @@ -510,6 +529,18 @@ public void callback(String notificationName, HashMap userInfo) engine.postEngineNotification(EngineNotifications.KEYCLOAK_UPDATE_REQUIRED, engineInfo); break; } + case DownloadNotifications.NOTIFICATION_PUSH_REGISTER_FAILED_BAD_DEVICE_UID_TO_REPLACE: { + Identity ownedIdentity = (Identity) userInfo.get(DownloadNotifications.NOTIFICATION_PUSH_REGISTER_FAILED_BAD_DEVICE_UID_TO_REPLACE_OWNED_IDENTITY_KEY); + if (ownedIdentity == null) { + break; + } + + HashMap engineInfo = new HashMap<>(); + engineInfo.put(EngineNotifications.PUSH_REGISTER_FAILED_BAD_DEVICE_UID_TO_REPLACE_BYTES_OWNED_IDENTITY_KEY, ownedIdentity.getBytes()); + + engine.postEngineNotification(EngineNotifications.PUSH_REGISTER_FAILED_BAD_DEVICE_UID_TO_REPLACE, engineInfo); + break; + } default: Logger.w("Received notification " + notificationName + " but no handler is set."); } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerGroups.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerGroups.java index f5c4e1b5..77fdc76c 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerGroups.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerGroups.java @@ -67,7 +67,8 @@ public void callback(String notificationName, HashMap userInfo) try (EngineSession engineSession = engine.getSession()) { byte[] groupOwnerAndUid = (byte[]) userInfo.get(IdentityNotifications.NOTIFICATION_GROUP_CREATED_GROUP_OWNER_AND_UID_KEY); Identity ownedIdentity = (Identity) userInfo.get(IdentityNotifications.NOTIFICATION_GROUP_CREATED_OWNED_IDENTITY_KEY); - if (groupOwnerAndUid == null || ownedIdentity == null) { + Boolean createdOnOtherDevice = (Boolean) userInfo.get(IdentityNotifications.NOTIFICATION_GROUP_CREATED_ON_OTHER_DEVICE_KEY); + if (groupOwnerAndUid == null || ownedIdentity == null || createdOnOtherDevice == null) { break; } @@ -122,6 +123,7 @@ public void callback(String notificationName, HashMap userInfo) engineInfo.put(EngineNotifications.GROUP_CREATED_GROUP_KEY, obvGroup); engineInfo.put(EngineNotifications.GROUP_CREATED_HAS_MULTIPLE_DETAILS_KEY, group.hasMultipleDetails()); engineInfo.put(EngineNotifications.GROUP_CREATED_PHOTO_URL_KEY, photoUrl); + engineInfo.put(EngineNotifications.GROUP_CREATED_ON_OTHER_DEVICE_KEY, createdOnOtherDevice); engine.postEngineNotification(EngineNotifications.GROUP_CREATED, engineInfo); } catch (Exception e) { e.printStackTrace(); diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerGroupsV2.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerGroupsV2.java index 38eb54a9..587704b9 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerGroupsV2.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerGroupsV2.java @@ -59,8 +59,9 @@ public void callback(String notificationName, HashMap userInfo) try (EngineSession engineSession = engine.getSession()) { Identity ownedIdentity = (Identity) userInfo.get(IdentityNotifications.NOTIFICATION_GROUP_V2_CREATED_OWNED_IDENTITY_KEY); GroupV2.Identifier groupIdentifier = (GroupV2.Identifier) userInfo.get(IdentityNotifications.NOTIFICATION_GROUP_V2_CREATED_GROUP_IDENTIFIER_KEY); - Boolean newGroup = (Boolean) userInfo.get(IdentityNotifications.NOTIFICATION_GROUP_V2_CREATED_NEW_GROUP_KEY); - if (ownedIdentity == null || groupIdentifier == null || newGroup == null) { + Boolean createdByMe = (Boolean) userInfo.get(IdentityNotifications.NOTIFICATION_GROUP_V2_CREATED_CREATED_BY_ME_KEY); + Boolean createdOnOtherDevice = (Boolean) userInfo.get(IdentityNotifications.NOTIFICATION_GROUP_V2_CREATED_ON_OTHER_DEVICE_KEY); + if (ownedIdentity == null || groupIdentifier == null || createdByMe == null || createdOnOtherDevice == null) { break; } @@ -71,8 +72,9 @@ public void callback(String notificationName, HashMap userInfo) HashMap engineInfo = new HashMap<>(); engineInfo.put(EngineNotifications.GROUP_V2_CREATED_OR_UPDATED_GROUP_KEY, obvGroupV2); - engineInfo.put(EngineNotifications.GROUP_V2_CREATED_OR_UPDATED_NEW_GROUP_KEY, newGroup); - engineInfo.put(EngineNotifications.GROUP_V2_CREATED_OR_UPDATED_BY_ME_KEY, newGroup); + engineInfo.put(EngineNotifications.GROUP_V2_CREATED_OR_UPDATED_NEW_GROUP_KEY, createdByMe); + engineInfo.put(EngineNotifications.GROUP_V2_CREATED_OR_UPDATED_BY_ME_KEY, createdByMe); + engineInfo.put(EngineNotifications.GROUP_V2_CREATED_OR_UPDATED_CREATED_ON_OTHER_DEVICE, createdOnOtherDevice); engine.postEngineNotification(EngineNotifications.GROUP_V2_CREATED_OR_UPDATED, engineInfo); } catch (Exception e) { e.printStackTrace(); @@ -97,6 +99,7 @@ public void callback(String notificationName, HashMap userInfo) engineInfo.put(EngineNotifications.GROUP_V2_CREATED_OR_UPDATED_GROUP_KEY, obvGroupV2); engineInfo.put(EngineNotifications.GROUP_V2_CREATED_OR_UPDATED_NEW_GROUP_KEY, false); engineInfo.put(EngineNotifications.GROUP_V2_CREATED_OR_UPDATED_BY_ME_KEY, updatedByMe); + engineInfo.put(EngineNotifications.GROUP_V2_CREATED_OR_UPDATED_CREATED_ON_OTHER_DEVICE, false); engine.postEngineNotification(EngineNotifications.GROUP_V2_CREATED_OR_UPDATED, engineInfo); } catch (Exception e) { e.printStackTrace(); diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerIdentity.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerIdentity.java index 5b7444bd..8e1893e8 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerIdentity.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/NotificationListenerIdentity.java @@ -61,6 +61,7 @@ void registerToNotifications(NotificationManager notificationManager) { IdentityNotifications.NOTIFICATION_CONTACT_CAPABILITIES_UPDATED, IdentityNotifications.NOTIFICATION_OWN_CAPABILITIES_UPDATED, IdentityNotifications.NOTIFICATION_CONTACT_ONE_TO_ONE_CHANGED, + IdentityNotifications.NOTIFICATION_OWNED_DEVICE_LIST_CHANGED, }) { notificationManager.addListener(notificationName, this); } @@ -339,6 +340,18 @@ public void callback(String notificationName, HashMap userInfo) engine.postEngineNotification(EngineNotifications.CONTACT_ONE_TO_ONE_CHANGED, engineInfo); break; } + case IdentityNotifications.NOTIFICATION_OWNED_DEVICE_LIST_CHANGED: { + Identity ownedIdentity = (Identity) userInfo.get(IdentityNotifications.NOTIFICATION_OWNED_DEVICE_LIST_CHANGED_OWNED_IDENTITY_KEY); + + if (ownedIdentity == null) { + break; + } + HashMap engineInfo = new HashMap<>(); + engineInfo.put(EngineNotifications.OWNED_IDENTITY_DEVICE_LIST_CHANGED_BYTES_OWNED_IDENTITY_KEY, ownedIdentity.getBytes()); + + engine.postEngineNotification(EngineNotifications.OWNED_IDENTITY_DEVICE_LIST_CHANGED, engineInfo); + break; + } default: Logger.w("Received notification " + notificationName + " but no handler is set."); } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/EngineAPI.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/EngineAPI.java index 6b65ad52..df999f24 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/EngineAPI.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/EngineAPI.java @@ -36,12 +36,15 @@ import io.olvid.engine.engine.types.identities.ObvIdentity; import io.olvid.engine.engine.types.identities.ObvKeycloakState; import io.olvid.engine.engine.types.identities.ObvMutualScanUrl; +import io.olvid.engine.engine.types.identities.ObvOwnedDevice; import io.olvid.engine.engine.types.identities.ObvTrustOrigin; +import io.olvid.engine.engine.types.sync.ObvSyncAtom; public interface EngineAPI { enum ApiKeyPermission { CALL, WEB_CLIENT, + MULTI_DEVICE, } enum ApiKeyStatus { @@ -67,15 +70,15 @@ enum ApiKeyStatus { String getServerOfIdentity(byte[] bytesIdentity); ObvIdentity[] getOwnedIdentities() throws Exception; ObvIdentity getOwnedIdentity(byte[] bytesOwnedIdentity) throws Exception; - ObvIdentity generateOwnedIdentity(String server, JsonIdentityDetails jsonIdentityDetails, UUID apiKey, ObvKeycloakState keycloakState); - UUID getApiKeyForOwnedIdentity(byte[] bytesOwnedIdentity); - boolean updateApiKeyForOwnedIdentity(byte[] bytesOwnedIdentity, UUID apiKey); + ObvIdentity generateOwnedIdentity(String server, JsonIdentityDetails jsonIdentityDetails, ObvKeycloakState keycloakState, String deviceDisplayName); + RegisterApiKeyResult registerOwnedIdentityApiKeyOnServer(byte[] bytesOwnedIdentity, UUID apiKey); void recreateServerSession(byte[] bytesOwnedIdentity); void deleteOwnedIdentity(byte[] bytesOwnedIdentity) throws Exception; JsonIdentityDetailsWithVersionAndPhoto[] getOwnedIdentityPublishedAndLatestDetails(byte[] bytesOwnedOdentity) throws Exception; ObvKeycloakState getOwnedIdentityKeycloakState(byte[] bytesOwnedIdentity) throws Exception; void saveKeycloakAuthState(byte[] bytesOwnedIdentity, String serializedAuthState) throws Exception; void saveKeycloakJwks(byte[] bytesOwnedIdentity, String serializedJwks) throws Exception; + void saveKeycloakApiKey(byte[] bytesOwnedIdentity, String apiKey) throws Exception; Collection getOwnedIdentitiesWithKeycloakPushTopic(String pushTopic) throws Exception; String getOwnedIdentityKeycloakUserId(byte[] bytesOwnedIdentity) throws Exception; void setOwnedIdentityKeycloakUserId(byte[] bytesOwnedIdentity, String id) throws Exception; @@ -89,10 +92,10 @@ enum ApiKeyStatus { String getOwnedIdentityKeycloakSelfRevocationTestNonce(byte[] bytesOwnedIdentity, String serverUrl); boolean updateKeycloakGroups(byte[] bytesOwnedIdentity, List signedGroupBlobs, List signedGroupDeletions, List signedGroupKicks, long keycloakCurrentTimestamp); - void registerToPushNotification(byte[] bytesOwnedIdentity, String firebaseToken, boolean kickOtherDevices, boolean useMultidevice) throws Exception; - void unregisterToPushNotification(byte[] bytesOwnedIdentity) throws Exception; + void registerToPushNotification(byte[] bytesOwnedIdentity, ObvPushNotificationType pushNotificationType, boolean reactivateCurrentDevice, byte[] bytesDeviceUidToReplace) throws Exception; void processAndroidPushNotification(String maskingUidString); byte[] getOwnedIdentityFromMaskingUid(String maskingUidString); + void processDeviceManagementRequest(byte[] bytesOwnedIdentity, ObvDeviceManagementRequest deviceManagementRequest) throws Exception; void updateLatestIdentityDetails(byte[] bytesOwnedIdentity, JsonIdentityDetails jsonIdentityDetails) throws Exception; void discardLatestIdentityDetails(byte[] bytesOwnedIdentity); @@ -102,6 +105,12 @@ enum ApiKeyStatus { byte[] getServerAuthenticationToken(byte[] bytesOwnedIdentity); List getOwnCapabilities(byte[] bytesOwnedIdentity); // returns null in case of error, empty list if there are no capabilities + List getOwnedDevices(byte[] bytesOwnedIdentity); + ObvDeviceList queryRegisteredOwnedDevicesFromServer(byte[] bytesOwnedIdentity); + void refreshOwnedDeviceList(byte[] bytesOwnedIdentity); + void recreateOwnedDeviceChannel(byte[] bytesOwnedIdentity, byte[] bytesDeviceUid); +// void resynchronizeAllOwnedDevices(byte[] bytesOwnedIdentity); + // ObvContactIdentity ObvIdentity[] getContactsOfOwnedIdentity(byte[] bytesOwnedIdentity) throws Exception; @@ -134,6 +143,7 @@ enum ApiKeyStatus { List getGroupsV2OfOwnedIdentity(byte[] bytesOwnedIdentity) throws Exception; void trustGroupV2PublishedDetails(byte[] bytesOwnedIdentity, byte[] bytesGroupIdentifier) throws Exception; ObvGroupV2.ObvGroupV2DetailsAndPhotos getGroupV2DetailsAndPhotos(byte[] bytesOwnedIdentity, byte[] bytesGroupIdentifier); + String getGroupV2JsonType(byte[] bytesOwnedIdentity, byte[] bytesGroupIdentifier); void initiateGroupV2Update(byte[] bytesOwnedIdentity, byte[] bytesGroupIdentifier, ObvGroupV2.ObvGroupV2ChangeSet changeSet) throws Exception; void leaveGroupV2(byte[] bytesOwnedIdentity, byte[] bytesGroupIdentifier) throws Exception; void disbandGroupV2(byte[] bytesOwnedIdentity, byte[] bytesGroupIdentifier) throws Exception; @@ -157,7 +167,7 @@ enum ApiKeyStatus { void startMutualScanTrustEstablishmentProtocol(byte[] bytesOwnedIdentity, byte[] bytesRemoteIdentity, byte[] signature) throws Exception; void startContactMutualIntroductionProtocol(byte[] bytesOwnedIdentity, byte[] bytesContactIdentityA, byte[][] bytesContactIdentities) throws Exception; void startGroupCreationProtocol(String serializedGroupDetailsWithVersionAndPhoto, String absolutePhotoUrl, byte[] bytesOwnedIdentity, byte[][] bytesRemoteIdentities) throws Exception; - void startGroupV2CreationProtocol(String serializedGroupDetails, String absolutePhotoUrl, byte[] bytesOwnedIdentity, HashSet ownPermissions, HashMap> otherGroupMembers) throws Exception; + void startGroupV2CreationProtocol(String serializedGroupDetails, String absolutePhotoUrl, byte[] bytesOwnedIdentity, HashSet ownPermissions, HashMap> otherGroupMembers, String serializedGroupType) throws Exception; void restartAllOngoingChannelEstablishmentProtocols(byte[] bytesOwnedIdentity, byte[] bytesContactIdentity) throws Exception; void recreateAllChannels(byte[] bytesOwnedIdentity, byte[] bytesContactIdentity) throws Exception; void inviteContactsToGroup(byte[] bytesOwnedIdentity, byte[] bytesGroupOwnerAndUid, byte[][] bytesNewMemberIdentities) throws Exception; @@ -168,7 +178,7 @@ enum ApiKeyStatus { void deleteContact(byte[] bytesOwnedIdentity, byte[] bytesContactIdentity) throws Exception; void downgradeOneToOneContact(byte[] bytesOwnedIdentity, byte[] bytesContactIdentity) throws Exception; void startOneToOneInvitationProtocol(byte[] bytesOwnedIdentity, byte[] bytesContactIdentity) throws Exception; - void deleteOwnedIdentityAndNotifyContacts(byte[] bytesOwnedIdentity) throws Exception; + void deleteOwnedIdentityAndNotifyContacts(byte[] bytesOwnedIdentity, boolean deleteEverywhere) throws Exception; void queryGroupOwnerForLatestGroupMembers(byte[] bytesGroupOwnerAndUid, byte[] bytesOwnedIdentity) throws Exception; void addKeycloakContact(byte[] bytesOwnedIdentity, byte[] bytesContactIdentity, String signedContactDetails) throws Exception; @@ -194,7 +204,7 @@ enum ApiKeyStatus { void markMessageForDeletion(byte[] bytesOwnedIdentity, byte[] messageIdentifier); void cancelAttachmentUpload(byte[] bytesOwnedIdentity, byte[] messageIdentifier, int attachmentNumber); void resendAllAttachmentNotifications() throws Exception; - void connectWebsocket(String os, String osVersion, int appBuild, String appVersion); + void connectWebsocket(boolean relyOnWebsocketForNetworkDetection, String os, String osVersion, int appBuild, String appVersion); void disconnectWebsocket(); void pingWebsocket(byte[] bytesOwnedIdentity); void retryScheduledNetworkTasks(); @@ -209,7 +219,7 @@ enum ApiKeyStatus { void discardBackup(byte[] backupKeyUid, int version); ObvBackupKeyVerificationOutput validateBackupSeed(String backupSeed, byte[] backupContent); ObvBackupKeyVerificationOutput verifyBackupSeed(String backupSeed); - ObvIdentity[] restoreOwnedIdentitiesFromBackup(String backupSeed, byte[] backupContent); + ObvIdentity[] restoreOwnedIdentitiesFromBackup(String backupSeed, byte[] backupContent, String deviceDisplayName); void restoreContactsAndGroupsFromBackup(String backupSeed, byte[] backupContent, ObvIdentity[] restoredOwnedIdentities); String decryptAppDataBackup(String backupSeed, byte[] backupContent); void appBackupSuccess(byte[] bytesBackupKeyUid, int version, String appBackupContent); @@ -226,9 +236,12 @@ enum ApiKeyStatus { String getOsmServerUrl(byte[] bytesOwnedIdentity); String getAddressServerUrl(byte[] bytesOwnedIdentity); + void propagateAppSyncAtomToAllOwnedIdentitiesOtherDevicesIfNeeded(ObvSyncAtom obvSyncAtom) throws Exception; + void propagateAppSyncAtomToOtherDevicesIfNeeded(byte[] bytesOwnedIdentity, ObvSyncAtom obvSyncAtom) throws Exception; // Run once after you upgrade from a version not handling Contact and ContactGroup UserData (profile photos) to a version able to do so void downloadAllUserData() throws Exception; + void setAllOwnedDeviceNames(String deviceName); void vacuumDatabase() throws Exception; } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/EngineNotifications.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/EngineNotifications.java index 9b8f62e9..119b43c2 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/EngineNotifications.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/EngineNotifications.java @@ -106,6 +106,8 @@ public abstract class EngineNotifications { public static final String GROUP_CREATED_GROUP_KEY = "group"; // ObvGroup public static final String GROUP_CREATED_HAS_MULTIPLE_DETAILS_KEY = "has_multiple_details"; // boolean public static final String GROUP_CREATED_PHOTO_URL_KEY = "photo_url"; // String + public static final String GROUP_CREATED_ON_OTHER_DEVICE_KEY = "on_other_device"; // boolean --> true if I am the group owner and the group was created on another device + public static final String GROUP_DELETED = "engine_notification_group_deleted"; public static final String GROUP_DELETED_BYTES_OWNED_IDENTITY_KEY = "bytes_owned_identity"; // byte[] @@ -303,6 +305,8 @@ public abstract class EngineNotifications { public static final String WEBSOCKET_CONNECTION_STATE_CHANGED = "engine_notification_websocket_connection_state_changed"; public static final String WEBSOCKET_CONNECTION_STATE_CHANGED_STATE_KEY = "state"; // int + public static final String WEBSOCKET_DETECTED_SOME_NETWORK = "engine_notification_websocket_detected_some_network"; + public static final String CONTACT_CAPABILITIES_UPDATED = "engine_notification_contact_capabilities_updated"; // List public static final String CONTACT_CAPABILITIES_UPDATED_BYTES_OWNED_IDENTITY_KEY = "bytes_owned_identity"; // byte[] public static final String CONTACT_CAPABILITIES_UPDATED_BYTES_CONTACT_IDENTITY_KEY = "bytes_contact_identity"; // byte[] @@ -324,8 +328,9 @@ public abstract class EngineNotifications { public static final String GROUP_V2_CREATED_OR_UPDATED = "engine_notification_group_v2_created_or_updated"; public static final String GROUP_V2_CREATED_OR_UPDATED_GROUP_KEY = "group"; // ObvGroupV2 - public static final String GROUP_V2_CREATED_OR_UPDATED_NEW_GROUP_KEY = "new_group"; // boolean + public static final String GROUP_V2_CREATED_OR_UPDATED_NEW_GROUP_KEY = "new_group"; // boolean --> if true, the group was created by be (as opposed to joined groups created by someone else) public static final String GROUP_V2_CREATED_OR_UPDATED_BY_ME_KEY = "by_me"; // boolean + public static final String GROUP_V2_CREATED_OR_UPDATED_CREATED_ON_OTHER_DEVICE = "created_on_other_device"; // boolean --> only meaningful for new groups created by be ("new_group" == true). true if created on another device, false if created on this device public static final String GROUP_V2_PHOTO_CHANGED = "engine_notification_group_v2_photo_changed"; public static final String GROUP_V2_PHOTO_CHANGED_BYTES_OWNED_IDENTITY_KEY = "bytes_owned_identity"; // byte[] @@ -357,4 +362,26 @@ public abstract class EngineNotifications { public static final String KEYCLOAK_GROUP_V2_SHARED_SETTINGS_BYTES_GROUP_IDENTIFIER_KEY = "bytes_group_identifier"; // byte[] public static final String KEYCLOAK_GROUP_V2_SHARED_SETTINGS_SHARED_SETTINGS_KEY = "shared_settings"; // String, serialized JsonSharedSettings public static final String KEYCLOAK_GROUP_V2_SHARED_SETTINGS_MODIFICATION_TIMESTAMP_KEY = "timestamp"; // long + + public static final String OWNED_IDENTITY_DELETED_FROM_ANOTHER_DEVICE = "engine_notification_owned_identity_deleted_from_another_device"; + public static final String OWNED_IDENTITY_DELETED_FROM_ANOTHER_DEVICE_BYTES_OWNED_IDENTITY_KEY = "bytes_owned_identity"; // byte[] + + public static final String OWNED_IDENTITY_DEVICE_LIST_CHANGED = "engine_notification_owned_identity_device_list_changed"; + public static final String OWNED_IDENTITY_DEVICE_LIST_CHANGED_BYTES_OWNED_IDENTITY_KEY = "bytes_owned_identity"; // byte[] + + public static final String KEYCLOAK_SYNCHRONIZATION_REQUIRED = "engine_notification_keycloak_synchronization_required"; + public static final String KEYCLOAK_SYNCHRONIZATION_REQUIRED_BYTES_OWNED_IDENTITY_KEY = "bytes_owned_identity"; // byte[] + + public static final String CONTACT_INTRODUCTION_INVITATION_SENT = "engine_notification_contact_introduction_invitation_sent"; + public static final String CONTACT_INTRODUCTION_INVITATION_SENT_BYTES_OWNED_IDENTITY_KEY = "bytes_owned_identity"; // byte[] + public static final String CONTACT_INTRODUCTION_INVITATION_SENT_BYTES_CONTACT_IDENTITY_A_KEY = "bytes_contact_identity_a"; // byte[] + public static final String CONTACT_INTRODUCTION_INVITATION_SENT_BYTES_CONTACT_IDENTITY_B_KEY = "bytes_contact_identity_b"; // byte[] + + public static final String CONTACT_INTRODUCTION_INVITATION_RESPONSE = "engine_notification_contact_introduction_invitation_response"; + public static final String CONTACT_INTRODUCTION_INVITATION_RESPONSE_BYTES_OWNED_IDENTITY_KEY = "bytes_owned_identity"; // byte[] + public static final String CONTACT_INTRODUCTION_INVITATION_RESPONSE_BYTES_MEDIATOR_IDENTITY_KEY = "bytes_mediator_identity"; // byte[] + public static final String CONTACT_INTRODUCTION_INVITATION_RESPONSE_CONTACT_SERIALIZED_DETAILS_KEY = "contact_serialized_Details"; // String + public static final String CONTACT_INTRODUCTION_INVITATION_RESPONSE_ACCEPTED_KEY = "accepted"; // boolean + public static final String PUSH_REGISTER_FAILED_BAD_DEVICE_UID_TO_REPLACE = "engine_notification_push_register_failed_bad_device_uid_to_replace"; + public static final String PUSH_REGISTER_FAILED_BAD_DEVICE_UID_TO_REPLACE_BYTES_OWNED_IDENTITY_KEY = "bytes_owned_identity"; } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/JsonGroupType.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/JsonGroupType.java new file mode 100644 index 00000000..5a80895f --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/JsonGroupType.java @@ -0,0 +1,116 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.engine.types; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class JsonGroupType { + public static final String TYPE_SIMPLE = "simple"; + public static final String TYPE_PRIVATE = "private"; + public static final String TYPE_READ_ONLY = "read_only"; + public static final String TYPE_CUSTOM = "custom"; + + public static final String REMOTE_DELETE_NOBODY = "nobody"; + public static final String REMOTE_DELETE_ADMINS = "admins"; + public static final String REMOTE_DELETE_EVERYONE = "everyone"; + + String type; + Boolean readOnly; + String remoteDelete; + + public JsonGroupType() { + } + + @JsonIgnore + public static JsonGroupType createSimple() { + return new JsonGroupType(TYPE_SIMPLE, null, null); + } + @JsonIgnore + public static JsonGroupType createPrivate() { + return new JsonGroupType(TYPE_PRIVATE, null, null); + } + @JsonIgnore + public static JsonGroupType createReadOnly() { + return new JsonGroupType(TYPE_READ_ONLY, null, null); + } + @JsonIgnore + public static JsonGroupType createCustom(boolean readOnly, String remoteDelete) { + if (remoteDelete == null || + !(remoteDelete.equals(REMOTE_DELETE_NOBODY) || remoteDelete.equals(REMOTE_DELETE_ADMINS) || remoteDelete.equals(REMOTE_DELETE_EVERYONE))) { + remoteDelete = REMOTE_DELETE_EVERYONE; + } + return new JsonGroupType(TYPE_CUSTOM, readOnly, remoteDelete); + } + + private JsonGroupType (String type, Boolean readOnly, String remoteDelete) { + this.type = type; + this.readOnly = readOnly; + this.remoteDelete = remoteDelete; + } + + @JsonProperty("type") + public String getType() { + return type; + } + + @JsonProperty("type") + public void setType(String type) { + this.type = type; + } + + @JsonProperty("ro") + public Boolean getReadOnly() { + return readOnly; + } + + @JsonProperty("ro") + public void setReadOnly(Boolean readOnly) { + this.readOnly = readOnly; + } + + @JsonProperty("del") + public String getRemoteDelete() { + return remoteDelete; + } + + @JsonProperty("del") + public void setRemoteDelete(String remoteDelete) { + this.remoteDelete = remoteDelete; + } + + @JsonIgnore + public boolean isEmpty() { + return type == null; + } + + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof JsonGroupType other)) { + return false; + } + return Objects.equals(type, other.type) && Objects.equals(readOnly, other.readOnly) && Objects.equals(remoteDelete, other.remoteDelete); + } +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvBytesKey.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvBytesKey.java index 1dc5af29..026ee63e 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvBytesKey.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvBytesKey.java @@ -19,9 +19,17 @@ package io.olvid.engine.engine.types; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; import java.util.Arrays; -public class ObvBytesKey { +public class ObvBytesKey implements Comparable { final byte[] bytes; public ObvBytesKey(byte[] bytes) { @@ -42,4 +50,39 @@ public boolean equals(Object other) { public int hashCode() { return Arrays.hashCode(bytes); } + + @Override + public int compareTo(ObvBytesKey other) { + if (bytes.length != other.bytes.length) { + return bytes.length - other.bytes.length; + } + for (int i=0; i { + @Override + public void serialize(ObvBytesKey value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeFieldName(serializers.getConfig().getBase64Variant().encode(value.bytes)); + } + } + + public static class KeyDeserializer extends com.fasterxml.jackson.databind.KeyDeserializer { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException { + return new ObvBytesKey(ctxt.getConfig().getBase64Variant().decode(key)); + } + } + + public static class Deserializer extends JsonDeserializer { + @Override + public ObvBytesKey deserialize(JsonParser p, DeserializationContext context) throws IOException { + return new ObvBytesKey(context.getConfig().getBase64Variant().decode(p.getValueAsString())); + } + } } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvDeviceList.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvDeviceList.java new file mode 100644 index 00000000..3505e102 --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvDeviceList.java @@ -0,0 +1,34 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.engine.types; + +import java.util.HashMap; + +import io.olvid.engine.engine.types.identities.ObvOwnedDevice; + +public class ObvDeviceList { + public final Boolean multiDevice; // null if the server is not able to determine if the user has multi-device permission + public final HashMap deviceUidsAndServerInfo; + + public ObvDeviceList(Boolean multiDevice, HashMap deviceUidsAndServerInfo) { + this.deviceUidsAndServerInfo = deviceUidsAndServerInfo; + this.multiDevice = multiDevice; + } +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvDeviceManagementRequest.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvDeviceManagementRequest.java new file mode 100644 index 00000000..e59d57c1 --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvDeviceManagementRequest.java @@ -0,0 +1,123 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.engine.types; + +import io.olvid.engine.datatypes.UID; +import io.olvid.engine.encoder.DecodingException; +import io.olvid.engine.encoder.Encoded; + +public class ObvDeviceManagementRequest { + public static final int ACTION_SET_NICKNAME = 0; + public static final int ACTION_DEACTIVATE_DEVICE = 1; + public static final int ACTION_SET_UNEXPIRING_DEVICE = 2; + + public final int action; + public final byte[] bytesDeviceUid; + public final String nickname; + + public static ObvDeviceManagementRequest createSetNicknameRequest(byte[] bytesDeviceUid, String nickname) { + if (nickname == null) { + nickname = ""; + } + return new ObvDeviceManagementRequest(ACTION_SET_NICKNAME, bytesDeviceUid, nickname); + } + + public static ObvDeviceManagementRequest createDeactivateDeviceRequest(byte[] bytesDeviceUid) { + return new ObvDeviceManagementRequest(ACTION_DEACTIVATE_DEVICE, bytesDeviceUid, null); + } + + public static ObvDeviceManagementRequest createSetUnexpiringDeviceRequest(byte[] bytesDeviceUid) { + return new ObvDeviceManagementRequest(ACTION_SET_UNEXPIRING_DEVICE, bytesDeviceUid, null); + } + + + private ObvDeviceManagementRequest(int action, byte[] bytesDeviceUid, String nickname) { + this.action = action; + this.bytesDeviceUid = bytesDeviceUid; + this.nickname = nickname; + } + + public UID getDeviceUid() { + if (bytesDeviceUid == null) { + return null; + } + return new UID(bytesDeviceUid); + } + + + public Encoded encode() { + switch (action) { + case ACTION_SET_NICKNAME: { + return Encoded.of(new Encoded[]{ + Encoded.of(ACTION_SET_NICKNAME), + Encoded.of(bytesDeviceUid), + Encoded.of(nickname), + }); + } + case ACTION_DEACTIVATE_DEVICE: { + return Encoded.of(new Encoded[]{ + Encoded.of(ACTION_DEACTIVATE_DEVICE), + Encoded.of(bytesDeviceUid), + }); + } + case ACTION_SET_UNEXPIRING_DEVICE: { + return Encoded.of(new Encoded[]{ + Encoded.of(ACTION_SET_UNEXPIRING_DEVICE), + Encoded.of(bytesDeviceUid), + }); + } + default: { + return null; + } + } + } + + public static ObvDeviceManagementRequest of(Encoded encoded) throws DecodingException { + Encoded[] encodeds = encoded.decodeList(); + int action = (int) encodeds[0].decodeLong(); + switch (action) { + case ACTION_SET_NICKNAME: { + if (encodeds.length != 3) { + throw new DecodingException(); + } + UID deviceUid = encodeds[1].decodeUid(); + String nickname = encodeds[2].decodeString(); + return new ObvDeviceManagementRequest(ACTION_SET_NICKNAME, deviceUid.getBytes(), nickname); + } + case ACTION_DEACTIVATE_DEVICE: { + if (encodeds.length != 2) { + throw new DecodingException(); + } + UID deviceUid = encodeds[1].decodeUid(); + return new ObvDeviceManagementRequest(ACTION_DEACTIVATE_DEVICE, deviceUid.getBytes(), null); + } + case ACTION_SET_UNEXPIRING_DEVICE: { + if (encodeds.length != 2) { + throw new DecodingException(); + } + UID deviceUid = encodeds[1].decodeUid(); + return new ObvDeviceManagementRequest(ACTION_SET_UNEXPIRING_DEVICE, deviceUid.getBytes(), null); + } + default: { + throw new DecodingException(); + } + } + } +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvDialog.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvDialog.java index 638d5acd..2d602b97 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvDialog.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvDialog.java @@ -27,6 +27,7 @@ import io.olvid.engine.encoder.Encoded; import io.olvid.engine.engine.types.identities.ObvGroupV2; import io.olvid.engine.engine.types.identities.ObvIdentity; +import io.olvid.engine.engine.types.sync.ObvSyncAtom; public class ObvDialog { private final UUID uuid; @@ -161,6 +162,7 @@ public static class Category { public static final int ACCEPT_ONE_TO_ONE_INVITATION_DIALOG_CATEGORY = 14; public static final int GROUP_V2_INVITATION_DIALOG_CATEGORY = 15; public static final int GROUP_V2_FROZEN_INVITATION_DIALOG_CATEGORY = 16; + public static final int SYNC_ITEM_TO_APPLY_DIALOG_CATEGORY = 17; private final int id; private final byte[] bytesContactIdentity; @@ -173,9 +175,10 @@ public static class Category { private final ObvIdentity[] pendingGroupMemberIdentities; public final Long serverTimestamp; private final ObvGroupV2 obvGroupV2; + private final ObvSyncAtom obvSyncAtom; - public Category(int id, byte[] bytesContactIdentity, String contactDisplayNameOrSerializedDetails, byte[] sasToDisplay, byte[] sasEntered, byte[] bytesMediatorOrGroupOwnerIdentity, String serializedGroupDetails, byte[] bytesGroupUid, ObvIdentity[] pendingGroupMemberIdentities, Long serverTimestamp, ObvGroupV2 obvGroupV2) { + public Category(int id, byte[] bytesContactIdentity, String contactDisplayNameOrSerializedDetails, byte[] sasToDisplay, byte[] sasEntered, byte[] bytesMediatorOrGroupOwnerIdentity, String serializedGroupDetails, byte[] bytesGroupUid, ObvIdentity[] pendingGroupMemberIdentities, Long serverTimestamp, ObvGroupV2 obvGroupV2, ObvSyncAtom obvSyncAtom) { this.id = id; this.bytesContactIdentity = bytesContactIdentity; this.contactDisplayNameOrSerializedDetails = contactDisplayNameOrSerializedDetails; @@ -187,6 +190,7 @@ public Category(int id, byte[] bytesContactIdentity, String contactDisplayNameOr this.pendingGroupMemberIdentities = pendingGroupMemberIdentities; this.serverTimestamp = serverTimestamp; this.obvGroupV2 = obvGroupV2; + this.obvSyncAtom = obvSyncAtom; } public int getId() { @@ -233,6 +237,10 @@ public ObvGroupV2 getObvGroupV2() { return obvGroupV2; } + public ObvSyncAtom getObvSyncItem() { + return obvSyncAtom; + } + private static Category of(Encoded encoded, ObjectMapper jsonObjectMapper) throws Exception { Encoded[] list = encoded.decodeList(); if (list.length != 2) { @@ -249,6 +257,7 @@ private static Category of(Encoded encoded, ObjectMapper jsonObjectMapper) throw ObvIdentity[] pendingGroupMemberIdentities = null; Long serverTimestamp = null; ObvGroupV2 obvGroupV2 = null; + ObvSyncAtom obvSyncAtom = null; Encoded[] vars = list[1].decodeList(); switch (id) { @@ -342,6 +351,13 @@ private static Category of(Encoded encoded, ObjectMapper jsonObjectMapper) throw obvGroupV2 = ObvGroupV2.of(vars[1]); break; } + case SYNC_ITEM_TO_APPLY_DIALOG_CATEGORY: { + if (vars.length != 1) { + throw new DecodingException(); + } + obvSyncAtom = ObvSyncAtom.of(vars[0]); + break; + } default: if (vars.length != 2) { throw new DecodingException(); @@ -350,7 +366,7 @@ private static Category of(Encoded encoded, ObjectMapper jsonObjectMapper) throw contactDisplayNameOrSerializedDetails = vars[1].decodeString(); break; } - return new Category(id, bytesContactIdentity, contactDisplayNameOrSerializedDetails, sasToDisplay, sasEntered, bytesMediatorOrGroupOwnerIdentity, serializedGroupDetails, bytesGroupUid, pendingGroupMemberIdentities, serverTimestamp, obvGroupV2); + return new Category(id, bytesContactIdentity, contactDisplayNameOrSerializedDetails, sasToDisplay, sasEntered, bytesMediatorOrGroupOwnerIdentity, serializedGroupDetails, bytesGroupUid, pendingGroupMemberIdentities, serverTimestamp, obvGroupV2, obvSyncAtom); } private Encoded encode(ObjectMapper jsonObjectMapper) { @@ -447,6 +463,11 @@ private Encoded encode(ObjectMapper jsonObjectMapper) { obvGroupV2.encode(), }); break; + case SYNC_ITEM_TO_APPLY_DIALOG_CATEGORY: + encodedVars = Encoded.of(new Encoded[]{ + obvSyncAtom.encode(), + }); + break; } return Encoded.of(new Encoded[]{ Encoded.of(id), @@ -455,51 +476,55 @@ private Encoded encode(ObjectMapper jsonObjectMapper) { } public static Category createInviteSent(byte[] bytesContactIdentity, String contactDisplayNameOrSerializedDetails) { - return new Category(INVITE_SENT_DIALOG_CATEGORY, bytesContactIdentity, contactDisplayNameOrSerializedDetails, null, null, null, null, null, null, null, null); + return new Category(INVITE_SENT_DIALOG_CATEGORY, bytesContactIdentity, contactDisplayNameOrSerializedDetails, null, null, null, null, null, null, null, null, null); } public static Category createAcceptInvite(byte[] bytesContactIdentity, String contactDisplayNameOrSerializedDetails, long serverTimestamp) { - return new Category(ACCEPT_INVITE_DIALOG_CATEGORY, bytesContactIdentity, contactDisplayNameOrSerializedDetails, null, null, null, null, null, null, serverTimestamp, null); + return new Category(ACCEPT_INVITE_DIALOG_CATEGORY, bytesContactIdentity, contactDisplayNameOrSerializedDetails, null, null, null, null, null, null, serverTimestamp, null, null); } public static Category createSasExchange(byte[] bytesContactIdentity, String contactDisplayNameOrSerializedDetails, byte[] sasToDisplay, long serverTimestamp) { - return new Category(SAS_EXCHANGE_DIALOG_CATEGORY, bytesContactIdentity, contactDisplayNameOrSerializedDetails, sasToDisplay, null, null, null, null, null, serverTimestamp, null); + return new Category(SAS_EXCHANGE_DIALOG_CATEGORY, bytesContactIdentity, contactDisplayNameOrSerializedDetails, sasToDisplay, null, null, null, null, null, serverTimestamp, null, null); } public static Category createSasConfirmed(byte[] bytesContactIdentity, String contactDisplayNameOrSerializedDetails, byte[] sasToDisplay, byte[] sasEntered) { - return new Category(SAS_CONFIRMED_DIALOG_CATEGORY, bytesContactIdentity, contactDisplayNameOrSerializedDetails, sasToDisplay, sasEntered, null, null, null, null, null, null); + return new Category(SAS_CONFIRMED_DIALOG_CATEGORY, bytesContactIdentity, contactDisplayNameOrSerializedDetails, sasToDisplay, sasEntered, null, null, null, null, null, null, null); } public static Category createInviteAccepted(byte[] bytesContactIdentity, String contactDisplayNameOrSerializedDetails) { - return new Category(INVITE_ACCEPTED_DIALOG_CATEGORY, bytesContactIdentity, contactDisplayNameOrSerializedDetails, null, null, null, null, null, null, null, null); + return new Category(INVITE_ACCEPTED_DIALOG_CATEGORY, bytesContactIdentity, contactDisplayNameOrSerializedDetails, null, null, null, null, null, null, null, null, null); } public static Category createAcceptMediatorInvite(byte[] bytesContactIdentity, String contactDisplayNameOrSerializedDetails, byte[] bytesMediatorIdentity, long serverTimestamp) { - return new Category(ACCEPT_MEDIATOR_INVITE_DIALOG_CATEGORY, bytesContactIdentity, contactDisplayNameOrSerializedDetails, null, null, bytesMediatorIdentity, null, null, null, serverTimestamp, null); + return new Category(ACCEPT_MEDIATOR_INVITE_DIALOG_CATEGORY, bytesContactIdentity, contactDisplayNameOrSerializedDetails, null, null, bytesMediatorIdentity, null, null, null, serverTimestamp, null, null); } public static Category createMediatorInviteAccepted(byte[] bytesContactIdentity, String contactDisplayNameOrSerializedDetails, byte[] bytesMediatorIdentity) { - return new Category(MEDIATOR_INVITE_ACCEPTED_DIALOG_CATEGORY, bytesContactIdentity, contactDisplayNameOrSerializedDetails, null, null, bytesMediatorIdentity, null, null, null, null, null); + return new Category(MEDIATOR_INVITE_ACCEPTED_DIALOG_CATEGORY, bytesContactIdentity, contactDisplayNameOrSerializedDetails, null, null, bytesMediatorIdentity, null, null, null, null, null, null); } public static Category createAcceptGroupInvite(String serializedGroupDetails, byte[] groupId, byte[] bytesGroupOwnerIdentity, ObvIdentity[] pendingGroupMemberIdentities, long serverTimestamp) { - return new Category(ACCEPT_GROUP_INVITE_DIALOG_CATEGORY, null, null, null, null, bytesGroupOwnerIdentity, serializedGroupDetails, groupId, pendingGroupMemberIdentities, serverTimestamp, null); + return new Category(ACCEPT_GROUP_INVITE_DIALOG_CATEGORY, null, null, null, null, bytesGroupOwnerIdentity, serializedGroupDetails, groupId, pendingGroupMemberIdentities, serverTimestamp, null, null); } public static Category createOneToOneInvitationSent(byte[] bytesContactIdentity) { - return new Category(ONE_TO_ONE_INVITATION_SENT_DIALOG_CATEGORY, bytesContactIdentity, null, null, null, null, null, null, null, null, null); + return new Category(ONE_TO_ONE_INVITATION_SENT_DIALOG_CATEGORY, bytesContactIdentity, null, null, null, null, null, null, null, null, null, null); } public static Category createAcceptOneToOneInvitation(byte[] bytesContactIdentity, Long serverTimestamp) { - return new Category(ACCEPT_ONE_TO_ONE_INVITATION_DIALOG_CATEGORY, bytesContactIdentity, null, null, null, null, null, null, null, serverTimestamp, null); + return new Category(ACCEPT_ONE_TO_ONE_INVITATION_DIALOG_CATEGORY, bytesContactIdentity, null, null, null, null, null, null, null, serverTimestamp, null, null); } public static Category createGroupV2Invitation(byte[] bytesInviterIdentity, ObvGroupV2 obvGroupV2) { - return new Category(GROUP_V2_INVITATION_DIALOG_CATEGORY, null, null, null, null, bytesInviterIdentity, null, null, null, null, obvGroupV2); + return new Category(GROUP_V2_INVITATION_DIALOG_CATEGORY, null, null, null, null, bytesInviterIdentity, null, null, null, null, obvGroupV2, null); } public static Category createGroupV2FrozenInvitation(byte[] bytesInviterIdentity, ObvGroupV2 obvGroupV2) { - return new Category(GROUP_V2_FROZEN_INVITATION_DIALOG_CATEGORY, null, null, null, null, bytesInviterIdentity, null, null, null, null, obvGroupV2); + return new Category(GROUP_V2_FROZEN_INVITATION_DIALOG_CATEGORY, null, null, null, null, bytesInviterIdentity, null, null, null, null, obvGroupV2, null); + } + + public static Category createSyncItemToApply(ObvSyncAtom obvSyncAtom) { + return new Category(SYNC_ITEM_TO_APPLY_DIALOG_CATEGORY, null, null, null, null, null, null, null, null, null, null, obvSyncAtom); } } } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvGroupOwnerAndUidKey.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvGroupOwnerAndUidKey.java new file mode 100644 index 00000000..37facb30 --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvGroupOwnerAndUidKey.java @@ -0,0 +1,87 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.engine.types; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.util.Arrays; + +import io.olvid.engine.datatypes.UID; + +public class ObvGroupOwnerAndUidKey { + public final byte[] groupOwner; + public final byte[] groupUid; + + public ObvGroupOwnerAndUidKey(byte[] groupOwnerAndUid) { + this.groupOwner = Arrays.copyOfRange(groupOwnerAndUid, 0, groupOwnerAndUid.length - UID.UID_LENGTH); + this.groupUid = Arrays.copyOfRange(groupOwnerAndUid, groupOwnerAndUid.length - UID.UID_LENGTH, groupOwnerAndUid.length); + } + + public ObvGroupOwnerAndUidKey(byte[] groupOwner, byte[] groupUid) { + this.groupOwner = groupOwner; + this.groupUid = groupUid; + } + + public byte[] getGroupOwnerAndUid() { + byte[] out = new byte[groupOwner.length + groupUid.length]; + System.arraycopy(groupOwner, 0, out, 0, groupOwner.length); + System.arraycopy(groupUid, 0, out, groupOwner.length, groupUid.length); + return out; + } + + + + @Override + public boolean equals(Object other) { + if (!(other instanceof ObvGroupOwnerAndUidKey)) return false; + return Arrays.equals(groupOwner, ((ObvGroupOwnerAndUidKey) other).groupOwner) + && Arrays.equals(groupUid, ((ObvGroupOwnerAndUidKey) other).groupUid); + } + + @Override + public int hashCode() { + return Arrays.hashCode(groupOwner) * 31 + Arrays.hashCode(groupUid); + } + + + + public static class Serializer extends JsonSerializer { + @Override + public void serialize(ObvGroupOwnerAndUidKey value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeFieldName(serializers.getConfig().getBase64Variant().encode(value.groupOwner) + "-" + serializers.getConfig().getBase64Variant().encode(value.groupUid)); + } + } + + public static class Deserializer extends KeyDeserializer { + @Override + public Object deserializeKey(String key, DeserializationContext context) throws IOException { + String[] parts = key.split("-"); + if (parts.length != 2) { + throw new IOException(); + } + return new ObvGroupOwnerAndUidKey(context.getConfig().getBase64Variant().decode(parts[0]), context.getConfig().getBase64Variant().decode(parts[1])); + } + } +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvPushNotificationType.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvPushNotificationType.java new file mode 100644 index 00000000..e2a7b4ce --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/ObvPushNotificationType.java @@ -0,0 +1,53 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.engine.types; + +public class ObvPushNotificationType { + public enum Platform { + ANDROID, + WINDOWS, + LINUX, + DAEMON, + } + + public final Platform platform; + public final String firebaseToken; + + private ObvPushNotificationType(Platform platform, String firebaseToken) { + this.platform = platform; + this.firebaseToken = firebaseToken; + } + + public static ObvPushNotificationType createAndroid(String firebaseToken) { + return new ObvPushNotificationType(Platform.ANDROID, firebaseToken); + } + + public static ObvPushNotificationType createWindows() { + return new ObvPushNotificationType(Platform.WINDOWS, null); + } + + public static ObvPushNotificationType createLinux() { + return new ObvPushNotificationType(Platform.LINUX, null); + } + + public static ObvPushNotificationType createDaemon() { + return new ObvPushNotificationType(Platform.DAEMON, null); + } +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/RegisterApiKeyResult.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/RegisterApiKeyResult.java new file mode 100644 index 00000000..830b08db --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/RegisterApiKeyResult.java @@ -0,0 +1,26 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.engine.types; + +public enum RegisterApiKeyResult { + SUCCESS, + INVALID_KEY, + FAILED, +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/SimpleEngineNotificationListener.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/SimpleEngineNotificationListener.java new file mode 100644 index 00000000..117eb59d --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/SimpleEngineNotificationListener.java @@ -0,0 +1,56 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.engine.types; + +import java.util.HashMap; +import java.util.Objects; + +public abstract class SimpleEngineNotificationListener implements EngineNotificationListener { + private final String notificationName; + private Long registrationNumber = null; + + public SimpleEngineNotificationListener(String notificationName) { + this.notificationName = notificationName; + } + + public abstract void callback(HashMap userInfo); + + @Override + public void callback(String notificationName, HashMap userInfo) { + if (Objects.equals(notificationName, this.notificationName)) { + callback(userInfo); + } + } + + @Override + public void setEngineNotificationListenerRegistrationNumber(long registrationNumber) { + this.registrationNumber = registrationNumber; + } + + @Override + public long getEngineNotificationListenerRegistrationNumber() { + return registrationNumber; + } + + @Override + public boolean hasEngineNotificationListenerRegistrationNumber() { + return registrationNumber != null; + } +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/identities/ObvGroupV2.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/identities/ObvGroupV2.java index 3592f88f..32d5129b 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/identities/ObvGroupV2.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/identities/ObvGroupV2.java @@ -238,6 +238,7 @@ public static class ObvGroupV2ChangeSet { public final HashMap> permissionChanges; // may contain your ownedIdentity public String updatedSerializedGroupDetails; // null if no change + public String updatedJsonGroupType; // null if no change public String updatedPhotoUrl; // null if no change, "" if photo removed public ObvGroupV2ChangeSet() { @@ -245,11 +246,12 @@ public ObvGroupV2ChangeSet() { addedMembersWithPermissions = new HashMap<>(); permissionChanges = new HashMap<>(); updatedSerializedGroupDetails = null; + updatedJsonGroupType = null; updatedPhotoUrl = null; } public boolean isEmpty() { - return removedMembers.isEmpty() && addedMembersWithPermissions.isEmpty() && permissionChanges.isEmpty() && updatedPhotoUrl == null && updatedSerializedGroupDetails == null; + return removedMembers.isEmpty() && addedMembersWithPermissions.isEmpty() && permissionChanges.isEmpty() && updatedPhotoUrl == null && updatedSerializedGroupDetails == null && updatedJsonGroupType == null; } public Encoded encode() { @@ -286,6 +288,9 @@ public Encoded encode() { if (updatedSerializedGroupDetails != null) { dic.put(new DictionaryKey("gd"), Encoded.of(updatedSerializedGroupDetails)); } + if (updatedJsonGroupType != null) { + dic.put(new DictionaryKey("gt"), Encoded.of(updatedJsonGroupType)); + } if (updatedPhotoUrl != null) { dic.put(new DictionaryKey("pu"), Encoded.of(updatedPhotoUrl)); } @@ -320,6 +325,10 @@ public static ObvGroupV2ChangeSet of(Encoded encoded) throws DecodingException { if (enc != null) { changeSet.updatedSerializedGroupDetails = enc.decodeString(); } + enc = dic.get(new DictionaryKey("gt")); + if (enc != null) { + changeSet.updatedJsonGroupType = enc.decodeString(); + } enc = dic.get(new DictionaryKey("pu")); if (enc != null) { changeSet.updatedPhotoUrl = enc.decodeString(); diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/identities/ObvIdentity.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/identities/ObvIdentity.java index 4ba42478..c9d0fb0d 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/identities/ObvIdentity.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/identities/ObvIdentity.java @@ -112,4 +112,8 @@ public boolean equals(Object other) { public int compareTo(ObvIdentity o) { return identity.computeUniqueUid().compareTo(o.identity.computeUniqueUid()); } + + public Identity getIdentity() { + return identity; + } } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/identities/ObvKeycloakState.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/identities/ObvKeycloakState.java index 626e27f2..9b4f6798 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/identities/ObvKeycloakState.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/identities/ObvKeycloakState.java @@ -31,22 +31,24 @@ import io.olvid.engine.encoder.Encoded; public class ObvKeycloakState { - public final String keycloakServer; - public final String clientId; - public final String clientSecret; - public final JsonWebKeySet jwks; - public final JsonWebKey signatureKey; - public final String serializedAuthState; + public final String keycloakServer; // non-null + public final String clientId; // non-null + public final String clientSecret; // may be null if keycloak is not configured with confidential access + public final JsonWebKeySet jwks; // non-null --> only set to null when sending to app and deserialization failed + public final JsonWebKey signatureKey; // non-null --> only set to null when sending to app and deserialization failed + public final String serializedAuthState; // device dependant --> do not share with other devices + public final String ownApiKey; // not included in the serialized version public final long latestRevocationListTimestamp; // not included in the serialized version public final long latestGroupUpdateTimestamp; // not included in the serialized version - public ObvKeycloakState(String keycloakServer, String clientId, String clientSecret, JsonWebKeySet jwks, JsonWebKey signatureKey, String serializedAuthState, long latestRevocationListTimestamp, long latestGroupUpdateTimestamp) { + public ObvKeycloakState(String keycloakServer, String clientId, String clientSecret, JsonWebKeySet jwks, JsonWebKey signatureKey, String serializedAuthState, String ownApiKey, long latestRevocationListTimestamp, long latestGroupUpdateTimestamp) { this.keycloakServer = keycloakServer; this.clientId = clientId; this.clientSecret = clientSecret; this.jwks = jwks; this.signatureKey = signatureKey; this.serializedAuthState = serializedAuthState; + this.ownApiKey = ownApiKey; this.latestRevocationListTimestamp = latestRevocationListTimestamp; this.latestGroupUpdateTimestamp = latestGroupUpdateTimestamp; } @@ -134,6 +136,6 @@ public static ObvKeycloakState of(Encoded encoded) throws DecodingException { serializedAuthState = null; } - return new ObvKeycloakState(keycloakServer, clientId, clientSecret, jwks, signatureKey, serializedAuthState, 0, 0); + return new ObvKeycloakState(keycloakServer, clientId, clientSecret, jwks, signatureKey, serializedAuthState, null, 0, 0); } } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/identities/ObvOwnedDevice.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/identities/ObvOwnedDevice.java new file mode 100644 index 00000000..f1cf0d74 --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/identities/ObvOwnedDevice.java @@ -0,0 +1,62 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.engine.types.identities; + +import java.util.Objects; + +public class ObvOwnedDevice { + public final byte[] bytesOwnedIdentity; + public final byte[] bytesDeviceUid; + public final ServerDeviceInfo serverDeviceInfo; + public final boolean currentDevice; + public final boolean channelConfirmed; + + public ObvOwnedDevice(byte[] bytesOwnedIdentity, byte[] bytesDeviceUid, ServerDeviceInfo serverDeviceInfo, boolean currentDevice, boolean channelConfirmed) { + this.bytesOwnedIdentity = bytesOwnedIdentity; + this.bytesDeviceUid = bytesDeviceUid; + this.serverDeviceInfo = serverDeviceInfo; + this.currentDevice = currentDevice; + this.channelConfirmed = channelConfirmed; + } + + public static class ServerDeviceInfo { + public final String displayName; + public final Long expirationTimestamp; + public final Long lastRegistrationTimestamp; + + public ServerDeviceInfo(String displayName, Long expirationTimestamp, Long lastRegistrationTimestamp) { + this.displayName = displayName; + this.expirationTimestamp = expirationTimestamp; + this.lastRegistrationTimestamp = lastRegistrationTimestamp; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof ServerDeviceInfo) { + ServerDeviceInfo other = (ServerDeviceInfo) obj; + return Objects.equals(displayName, other.displayName) + && Objects.equals(expirationTimestamp, other.expirationTimestamp) + && Objects.equals(lastRegistrationTimestamp, other.lastRegistrationTimestamp); + } + return false; + } + } + +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvBackupAndSyncDelegate.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvBackupAndSyncDelegate.java new file mode 100644 index 00000000..ced21ecf --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvBackupAndSyncDelegate.java @@ -0,0 +1,41 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.engine.types.sync; + +import io.olvid.engine.datatypes.Identity; + +public interface ObvBackupAndSyncDelegate { + ////// + // Return a tag corresponding to this delegate + String getTag(); + + ////// + // This method computes a snapshot of the data to sync + ObvSyncSnapshotNode getSyncSnapshot(Identity ownedIdentity); + + + ////// + // Method used to deserialize a node that was serialized with ObvSyncSnapshotNode.serialize(ObjectMapper jsonObjectMapper) + byte[] serialize(ObvSyncSnapshotNode snapshotNode) throws Exception; + ////// + // Method used to deserialize a node that was serialized with ObvSyncSnapshotNode.serialize(ObjectMapper jsonObjectMapper) + ObvSyncSnapshotNode deserialize(byte[] serializedSnapshotNode) throws Exception; + +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvSyncAtom.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvSyncAtom.java new file mode 100644 index 00000000..769f34f3 --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvSyncAtom.java @@ -0,0 +1,475 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.engine.types.sync; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.datatypes.UID; +import io.olvid.engine.datatypes.containers.GroupV2; +import io.olvid.engine.encoder.DecodingException; +import io.olvid.engine.encoder.Encoded; + +public class ObvSyncAtom { + public static final int TYPE_CONTACT_NICKNAME_CHANGE = 0; + public static final int TYPE_GROUP_V1_NICKNAME_CHANGE = 1; + public static final int TYPE_GROUP_V2_NICKNAME_CHANGE = 2; + public static final int TYPE_CONTACT_PERSONAL_NOTE_CHANGE = 3; + public static final int TYPE_GROUP_V1_PERSONAL_NOTE_CHANGE = 4; + public static final int TYPE_GROUP_V2_PERSONAL_NOTE_CHANGE = 5; + public static final int TYPE_OWN_PROFILE_NICKNAME_CHANGE = 6; + public static final int TYPE_CONTACT_CUSTOM_HUE_CHANGE = 7; + public static final int TYPE_CONTACT_SEND_READ_RECEIPT_CHANGE = 8; + public static final int TYPE_GROUP_V1_SEND_READ_RECEIPT_CHANGE = 9; + public static final int TYPE_GROUP_V2_SEND_READ_RECEIPT_CHANGE = 10; + public static final int TYPE_PINNED_DISCUSSIONS_CHANGE = 11; + public static final int TYPE_TRUST_CONTACT_DETAILS = 12; + public static final int TYPE_TRUST_GROUP_V1_DETAILS = 13; + public static final int TYPE_TRUST_GROUP_V2_DETAILS = 14; + public static final int TYPE_SETTING_DEFAULT_SEND_READ_RECEIPTS = 15; + public static final int TYPE_SETTING_AUTO_JOIN_GROUPS = 16; + + + + public final int syncType; + private final Identity contactIdentity; + private final byte[] groupOwnerAndUid; + private final byte[] bytesGroupIdentifier; + private final String stringValue; + private final Integer integerValue; + private final Boolean booleanValue; + private final List discussionIdentifiers; + + private ObvSyncAtom(int syncType, Identity contactIdentity, byte[] groupOwnerAndUid, byte[] bytesGroupIdentifier, String stringValue, Integer integerValue, Boolean booleanValue, List discussionIdentifiers) { + this.syncType = syncType; + this.contactIdentity = contactIdentity; + this.groupOwnerAndUid = groupOwnerAndUid; + this.bytesGroupIdentifier = bytesGroupIdentifier; + this.stringValue = stringValue; + this.integerValue = integerValue; + this.booleanValue = booleanValue; + this.discussionIdentifiers = discussionIdentifiers; + } + + public static ObvSyncAtom createContactNicknameChange(byte[] bytesContactIdentity, String nickname) throws DecodingException { + return new ObvSyncAtom(TYPE_CONTACT_NICKNAME_CHANGE, Identity.of(bytesContactIdentity), null, null, nickname, null, null, null); + } + + public static ObvSyncAtom createGroupV1NicknameChange(byte[] bytesGroupOwnerAndUid, String nickname) { + return new ObvSyncAtom(TYPE_GROUP_V1_NICKNAME_CHANGE, null, bytesGroupOwnerAndUid, null, nickname, null, null, null); + } + + public static ObvSyncAtom createGroupV2NicknameChange(byte[] bytesGroupV2Identifier, String nickname) throws DecodingException { + return new ObvSyncAtom(TYPE_GROUP_V2_NICKNAME_CHANGE, null, null, bytesGroupV2Identifier, nickname, null, null, null); + } + + public static ObvSyncAtom createContactPersonalNoteChange(byte[] bytesContactIdentity, String personalNote) throws DecodingException { + return new ObvSyncAtom(TYPE_CONTACT_PERSONAL_NOTE_CHANGE, Identity.of(bytesContactIdentity), null, null, personalNote, null, null, null); + } + + public static ObvSyncAtom createGroupV1PersonalNoteChange(byte[] bytesGroupOwnerAndUid, String nickname) { + return new ObvSyncAtom(TYPE_GROUP_V1_PERSONAL_NOTE_CHANGE, null, bytesGroupOwnerAndUid, null, nickname, null, null, null); + } + + public static ObvSyncAtom createGroupV2PersonalNoteChange(byte[] bytesGroupV2Identifier, String nickname) throws DecodingException { + return new ObvSyncAtom(TYPE_GROUP_V2_PERSONAL_NOTE_CHANGE, null, null, bytesGroupV2Identifier, nickname, null, null, null); + } + + public static ObvSyncAtom createOwnProfileNicknameChange(String nickname) { + return new ObvSyncAtom(TYPE_OWN_PROFILE_NICKNAME_CHANGE, null, null, null, nickname, null, null, null); + } + + public static ObvSyncAtom createContactCustomHueChange(byte[] bytesContactIdentity, Integer customHue) throws DecodingException { + return new ObvSyncAtom(TYPE_CONTACT_CUSTOM_HUE_CHANGE, Identity.of(bytesContactIdentity), null, null, null, customHue, null, null); + } + + public static ObvSyncAtom createContactSendReadReceiptChange(byte[] bytesContactIdentity, Boolean sendReadReceipt) throws DecodingException { + return new ObvSyncAtom(TYPE_CONTACT_SEND_READ_RECEIPT_CHANGE, Identity.of(bytesContactIdentity), null, null, null, null, sendReadReceipt, null); + } + + public static ObvSyncAtom createGroupV1SendReadReceiptChange(byte[] bytesGroupOwnerAndUid, Boolean sendReadReceipt) { + return new ObvSyncAtom(TYPE_GROUP_V1_SEND_READ_RECEIPT_CHANGE, null, bytesGroupOwnerAndUid, null, null, null, sendReadReceipt, null); + } + + public static ObvSyncAtom createGroupV2SendReadReceiptChange(byte[] bytesGroupV2Identifier, Boolean sendReadReceipt) throws DecodingException { + return new ObvSyncAtom(TYPE_GROUP_V2_SEND_READ_RECEIPT_CHANGE, null, null, bytesGroupV2Identifier, null, null, sendReadReceipt, null); + } + + public static ObvSyncAtom createPinnedDiscussionsChange(List discussionIdentifiers, boolean ordered) throws DecodingException { + return new ObvSyncAtom(TYPE_PINNED_DISCUSSIONS_CHANGE, null, null, null, null, null, ordered, discussionIdentifiers); + } + + // we send the complete details to trust in the ObvSyncAtom as the version may be meaningless (after a channel creation, published details may require a version number downgrade) + public static ObvSyncAtom createTrustContactDetails(Identity contactIdentity, String serializedIdentityDetailsWithVersionAndPhoto) { + return new ObvSyncAtom(TYPE_TRUST_CONTACT_DETAILS, contactIdentity, null, null, serializedIdentityDetailsWithVersionAndPhoto, null, null, null); + } + + // we send the complete details to trust in the ObvSyncAtom as the version may be meaningless (after a channel creation, published details may require a version number downgrade) + public static ObvSyncAtom createTrustGroupV1Details(byte[] bytesGroupOwnerAndUid, String serializedGroupDetailsWithVersionAndPhoto) { + return new ObvSyncAtom(TYPE_TRUST_GROUP_V1_DETAILS, null, bytesGroupOwnerAndUid, null, serializedGroupDetailsWithVersionAndPhoto, null, null, null); + } + + public static ObvSyncAtom createTrustGroupV2Details(GroupV2.Identifier groupIdentifier, int version) { + return new ObvSyncAtom(TYPE_TRUST_GROUP_V2_DETAILS, null, null, groupIdentifier.encode().getBytes(), null, version, null, null); + } + + public static ObvSyncAtom createSettingDefaultSendReadReceipts(boolean sendReadReceipt) { + return new ObvSyncAtom(TYPE_SETTING_DEFAULT_SEND_READ_RECEIPTS, null, null, null, null, null, sendReadReceipt, null); + } + + public static ObvSyncAtom createSettingAutoJoinGroups(String autoJoinGroupsType) { + return new ObvSyncAtom(TYPE_SETTING_AUTO_JOIN_GROUPS, null, null, null, autoJoinGroupsType, null, null, null); + } + + + public boolean isAppSyncItem() { + switch (syncType) { + case TYPE_CONTACT_NICKNAME_CHANGE: + case TYPE_GROUP_V1_NICKNAME_CHANGE: + case TYPE_GROUP_V2_NICKNAME_CHANGE: + case TYPE_CONTACT_PERSONAL_NOTE_CHANGE: + case TYPE_GROUP_V1_PERSONAL_NOTE_CHANGE: + case TYPE_GROUP_V2_PERSONAL_NOTE_CHANGE: + case TYPE_OWN_PROFILE_NICKNAME_CHANGE: + case TYPE_CONTACT_CUSTOM_HUE_CHANGE: + case TYPE_CONTACT_SEND_READ_RECEIPT_CHANGE: + case TYPE_GROUP_V1_SEND_READ_RECEIPT_CHANGE: + case TYPE_GROUP_V2_SEND_READ_RECEIPT_CHANGE: + case TYPE_PINNED_DISCUSSIONS_CHANGE: + case TYPE_SETTING_DEFAULT_SEND_READ_RECEIPTS: + case TYPE_SETTING_AUTO_JOIN_GROUPS: + return true; + case TYPE_TRUST_CONTACT_DETAILS: + case TYPE_TRUST_GROUP_V1_DETAILS: + case TYPE_TRUST_GROUP_V2_DETAILS: + default: + return false; + } + } + + public byte[] getBytesContactIdentity() { + return contactIdentity.getBytes(); + } + + public Identity getContactIdentity() { + return contactIdentity; + } + + public byte[] getBytesGroupOwnerAndUid() { + return groupOwnerAndUid; + } + + public byte[] getBytesGroupIdentifier() { + return bytesGroupIdentifier; + } + + public GroupV2.Identifier getGroupIdentifier() throws DecodingException { + return GroupV2.Identifier.of(bytesGroupIdentifier); + } + + public String getStringValue() { + if (stringValue == null) { + return null; + } + String out = stringValue.trim(); + if (out.length() == 0) { + return null; + } + return out; + } + + public Integer getIntegerValue() { + return integerValue; + } + + public Boolean getBooleanValue() { + return booleanValue; + } + + public List getDiscussionIdentifiers() { + return discussionIdentifiers; + } + + public Encoded encode() { + ArrayList encodeds = new ArrayList<>(); + encodeds.add(Encoded.of(syncType)); + switch (syncType) { + case TYPE_CONTACT_NICKNAME_CHANGE: + case TYPE_CONTACT_PERSONAL_NOTE_CHANGE: { + encodeds.add(Encoded.of(contactIdentity)); + if (stringValue != null) { + encodeds.add(Encoded.of(stringValue)); + } + break; + } + case TYPE_GROUP_V1_NICKNAME_CHANGE: + case TYPE_GROUP_V1_PERSONAL_NOTE_CHANGE: { + encodeds.add(Encoded.of(Arrays.copyOfRange(groupOwnerAndUid, 0, groupOwnerAndUid.length - UID.UID_LENGTH))); + encodeds.add(Encoded.of(Arrays.copyOfRange(groupOwnerAndUid, groupOwnerAndUid.length - UID.UID_LENGTH, groupOwnerAndUid.length))); + if (stringValue != null) { + encodeds.add(Encoded.of(stringValue)); + } + break; + } + case TYPE_GROUP_V2_NICKNAME_CHANGE: + case TYPE_GROUP_V2_PERSONAL_NOTE_CHANGE: { + encodeds.add(Encoded.of(bytesGroupIdentifier)); + if (stringValue != null) { + encodeds.add(Encoded.of(stringValue)); + } + break; + } + case TYPE_OWN_PROFILE_NICKNAME_CHANGE: { + if (stringValue != null) { + encodeds.add(Encoded.of(stringValue)); + } + break; + } + case TYPE_CONTACT_CUSTOM_HUE_CHANGE: { + encodeds.add(Encoded.of(contactIdentity)); + if (integerValue != null) { + encodeds.add(Encoded.of(integerValue)); + } + break; + } + case TYPE_CONTACT_SEND_READ_RECEIPT_CHANGE: { + encodeds.add(Encoded.of(contactIdentity)); + if (booleanValue != null) { + encodeds.add(Encoded.of(booleanValue)); + } + break; + } + case TYPE_GROUP_V1_SEND_READ_RECEIPT_CHANGE: { + encodeds.add(Encoded.of(Arrays.copyOfRange(groupOwnerAndUid, 0, groupOwnerAndUid.length - UID.UID_LENGTH))); + encodeds.add(Encoded.of(Arrays.copyOfRange(groupOwnerAndUid, groupOwnerAndUid.length - UID.UID_LENGTH, groupOwnerAndUid.length))); + if (booleanValue != null) { + encodeds.add(Encoded.of(booleanValue)); + } + break; + } + case TYPE_GROUP_V2_SEND_READ_RECEIPT_CHANGE: { + encodeds.add(Encoded.of(bytesGroupIdentifier)); + if (booleanValue != null) { + encodeds.add(Encoded.of(booleanValue)); + } + break; + } + case TYPE_PINNED_DISCUSSIONS_CHANGE: { + List encodedDiscussionIdentifiers = new ArrayList<>(); + for (DiscussionIdentifier discussionIdentifier : discussionIdentifiers) { + encodedDiscussionIdentifiers.add(discussionIdentifier.encode()); + } + encodeds.add(Encoded.of(encodedDiscussionIdentifiers.toArray(new Encoded[0]))); + encodeds.add(Encoded.of(booleanValue)); + break; + } + case TYPE_TRUST_CONTACT_DETAILS: { + encodeds.add(Encoded.of(contactIdentity)); + encodeds.add(Encoded.of(stringValue)); + break; + } + case TYPE_TRUST_GROUP_V1_DETAILS: { + encodeds.add(Encoded.of(Arrays.copyOfRange(groupOwnerAndUid, 0, groupOwnerAndUid.length - UID.UID_LENGTH))); + encodeds.add(Encoded.of(Arrays.copyOfRange(groupOwnerAndUid, groupOwnerAndUid.length - UID.UID_LENGTH, groupOwnerAndUid.length))); + encodeds.add(Encoded.of(stringValue)); + break; + } + case TYPE_TRUST_GROUP_V2_DETAILS: { + encodeds.add(Encoded.of(bytesGroupIdentifier)); + encodeds.add(Encoded.of(integerValue)); + break; + } + case TYPE_SETTING_DEFAULT_SEND_READ_RECEIPTS: { + encodeds.add(Encoded.of(booleanValue)); + break; + } + case TYPE_SETTING_AUTO_JOIN_GROUPS: { + encodeds.add(Encoded.of(stringValue)); + break; + } + default: { + return null; + } + } + return Encoded.of(encodeds.toArray(new Encoded[0])); + } + + public static ObvSyncAtom of(Encoded encoded) throws DecodingException { + Encoded[] encodeds = encoded.decodeList(); + if (encodeds.length == 0) { + throw new DecodingException(); + } + int syncType = (int) encodeds[0].decodeLong(); + switch (syncType) { + case TYPE_CONTACT_NICKNAME_CHANGE: + case TYPE_CONTACT_PERSONAL_NOTE_CHANGE: { + if (encodeds.length == 2) { + return new ObvSyncAtom(syncType, encodeds[1].decodeIdentity(), null, null, null, null, null, null); + } else if (encodeds.length == 3) { + return new ObvSyncAtom(syncType, encodeds[1].decodeIdentity(), null, null, encodeds[2].decodeString(), null, null, null); + } + break; + } + case TYPE_GROUP_V1_NICKNAME_CHANGE: + case TYPE_GROUP_V1_PERSONAL_NOTE_CHANGE: { + if (encodeds.length == 3) { + return new ObvSyncAtom(syncType, null, joinArrays(encodeds[1].decodeBytes(), encodeds[2].decodeBytes()), null, null, null, null, null); + } else if (encodeds.length == 4) { + return new ObvSyncAtom(syncType, null, joinArrays(encodeds[1].decodeBytes(), encodeds[2].decodeBytes()), null, encodeds[3].decodeString(), null, null, null); + } + break; + } + case TYPE_GROUP_V2_NICKNAME_CHANGE: + case TYPE_GROUP_V2_PERSONAL_NOTE_CHANGE: { + if (encodeds.length == 2) { + return new ObvSyncAtom(syncType, null, null, encodeds[1].decodeBytes(), null, null, null, null); + } else if (encodeds.length == 3) { + return new ObvSyncAtom(syncType, null, null, encodeds[1].decodeBytes(), encodeds[2].decodeString(), null, null, null); + } + break; + } + case TYPE_OWN_PROFILE_NICKNAME_CHANGE: { + if (encodeds.length == 1) { + return new ObvSyncAtom(syncType, null, null, null, null, null, null, null); + } else if (encodeds.length == 2) { + return new ObvSyncAtom(syncType, null, null, null, encodeds[1].decodeString(), null, null, null); + } + break; + } + case TYPE_CONTACT_CUSTOM_HUE_CHANGE: { + if (encodeds.length == 2) { + return new ObvSyncAtom(syncType, encodeds[1].decodeIdentity(), null, null, null, null, null, null); + } else if (encodeds.length == 3) { + return new ObvSyncAtom(syncType, encodeds[1].decodeIdentity(), null, null, null, (int) encodeds[2].decodeLong(), null, null); + } + break; + } + case TYPE_CONTACT_SEND_READ_RECEIPT_CHANGE: { + if (encodeds.length == 2) { + return new ObvSyncAtom(syncType, encodeds[1].decodeIdentity(), null, null, null, null, null, null); + } else if (encodeds.length == 3) { + return new ObvSyncAtom(syncType, encodeds[1].decodeIdentity(), null, null, null, null, encodeds[2].decodeBoolean(), null); + } + break; + } + case TYPE_GROUP_V1_SEND_READ_RECEIPT_CHANGE: { + if (encodeds.length == 3) { + return new ObvSyncAtom(syncType, null, joinArrays(encodeds[1].decodeBytes(), encodeds[2].decodeBytes()), null, null, null, null, null); + } else if (encodeds.length == 4) { + return new ObvSyncAtom(syncType, null, joinArrays(encodeds[1].decodeBytes(), encodeds[2].decodeBytes()), null, null, null, encodeds[3].decodeBoolean(), null); + } + break; + } + case TYPE_GROUP_V2_SEND_READ_RECEIPT_CHANGE: { + if (encodeds.length == 2) { + return new ObvSyncAtom(syncType, null, null, encodeds[1].decodeBytes(), null, null, null, null); + } else if (encodeds.length == 3) { + return new ObvSyncAtom(syncType, null, null, encodeds[1].decodeBytes(), null, null, encodeds[2].decodeBoolean(), null); + } + break; + } + case TYPE_PINNED_DISCUSSIONS_CHANGE: { + List discussionIdentifiers = new ArrayList<>(); + for (Encoded encodedDiscussionIdentifier : encodeds[1].decodeList()) { + discussionIdentifiers.add(DiscussionIdentifier.of(encodedDiscussionIdentifier)); + } + return new ObvSyncAtom(syncType, null, null, null, null, null, encodeds[2].decodeBoolean(), discussionIdentifiers); + } + case TYPE_TRUST_CONTACT_DETAILS: { + return new ObvSyncAtom(syncType, encodeds[1].decodeIdentity(), null, null, encodeds[2].decodeString(), null, null, null); + } + case TYPE_TRUST_GROUP_V1_DETAILS: { + return new ObvSyncAtom(syncType, null, joinArrays(encodeds[1].decodeBytes(), encodeds[2].decodeBytes()), null, encodeds[3].decodeString(), null, null, null); + } + case TYPE_TRUST_GROUP_V2_DETAILS: { + return new ObvSyncAtom(syncType, null, null, encodeds[1].decodeBytes(), null, (int) encodeds[2].decodeLong(), null, null); + } + case TYPE_SETTING_DEFAULT_SEND_READ_RECEIPTS: { + return new ObvSyncAtom(syncType, null, null, null, null, null, encodeds[1].decodeBoolean(), null); + } + case TYPE_SETTING_AUTO_JOIN_GROUPS: { + return new ObvSyncAtom(syncType, null, null, null, encodeds[1].decodeString(), null, null, null); + } + } + throw new DecodingException(); + } + + private static byte[] joinArrays(byte[] a, byte[] b) { + byte[] out = new byte[a.length + b.length]; + System.arraycopy(a, 0, out, 0, a.length); + System.arraycopy(b, 0, out, a.length, b.length); + return out; + } + + public static class DiscussionIdentifier { + public static final int CONTACT = 0; + public static final int GROUP_V1 = 1; + public static final int GROUP_V2 = 2; + + public final int type; + public final byte[] bytesDiscussionIdentifier; + + public DiscussionIdentifier(int type, byte[] bytesDiscussionIdentifier) { + this.type = type; + this.bytesDiscussionIdentifier = bytesDiscussionIdentifier; + } + + public Encoded encode() { + List encodeds = new ArrayList<>(); + encodeds.add(Encoded.of(type)); + switch (type) { + case CONTACT: + case GROUP_V2: { + encodeds.add(Encoded.of(bytesDiscussionIdentifier)); + break; + } + case GROUP_V1: { + encodeds.add(Encoded.of(Arrays.copyOfRange(bytesDiscussionIdentifier, 0, bytesDiscussionIdentifier.length - UID.UID_LENGTH))); + encodeds.add(Encoded.of(Arrays.copyOfRange(bytesDiscussionIdentifier, bytesDiscussionIdentifier.length - UID.UID_LENGTH, bytesDiscussionIdentifier.length))); + break; + } + default: + return null; + } + return Encoded.of(encodeds.toArray(new Encoded[0])); + } + + + public static DiscussionIdentifier of(Encoded encoded) throws DecodingException { + Encoded[] encodeds = encoded.decodeList(); + if (encodeds.length == 0) { + throw new DecodingException(); + } + int type = (int) encodeds[0].decodeLong(); + switch (type) { + case CONTACT: + case GROUP_V2: { + return new DiscussionIdentifier(type, encodeds[1].decodeBytes()); + } + case GROUP_V1: { + return new DiscussionIdentifier(type, joinArrays(encodeds[1].decodeBytes(), encodeds[2].decodeBytes())); + } + } + throw new DecodingException(); + } + } +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvSyncDiff.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvSyncDiff.java new file mode 100644 index 00000000..5565ffc8 --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvSyncDiff.java @@ -0,0 +1,53 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.engine.types.sync; + +public class ObvSyncDiff { + // TODO only used to notify the app, needs to be encodable to send to other device + public static final int TYPE_SETTING_AUTO_JOIN_GROUPS = 0; + public static final int TYPE_SETTING_SEND_READ_RECEIPT = 1; + + private final int diffType; + private boolean resolutionInProgress; + private final Boolean localBoolean; + private final Boolean otherBoolean; + private final String localString; + private final String otherString; + + public ObvSyncDiff(int diffType, Boolean localBoolean, Boolean otherBoolean, String localString, String otherString) { + this.diffType = diffType; + this.resolutionInProgress = false; + this.localBoolean = localBoolean; + this.otherBoolean = otherBoolean; + this.localString = localString; + this.otherString = otherString; + } + + public void markResolutionInProgress() { + this.resolutionInProgress = true; + } + + public static ObvSyncDiff createSettingAutoJoinGroups(String localValue, String otherValue) { + return new ObvSyncDiff(TYPE_SETTING_AUTO_JOIN_GROUPS, null, null, localValue, otherValue); + } + public static ObvSyncDiff createSettingSendReadReceipt(boolean localValue, boolean otherValue) { + return new ObvSyncDiff(TYPE_SETTING_SEND_READ_RECEIPT, localValue, otherValue, null, null); + } +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvSyncSnapshot.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvSyncSnapshot.java new file mode 100644 index 00000000..59f7fea3 --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvSyncSnapshot.java @@ -0,0 +1,107 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.engine.types.sync; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import io.olvid.engine.datatypes.DictionaryKey; +import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.encoder.Encoded; + +public class ObvSyncSnapshot { + private final HashMap snapshotMap; + + public ObvSyncSnapshot(HashMap snapshotMap) { + this.snapshotMap = snapshotMap; + } + + public static ObvSyncSnapshot get(Identity ownedIdentity, ObvBackupAndSyncDelegate... delegates) { + HashMap snapshotMap = new HashMap<>(); + for (ObvBackupAndSyncDelegate delegate : delegates) { + snapshotMap.put(delegate.getTag(), delegate.getSyncSnapshot(ownedIdentity)); + } + return new ObvSyncSnapshot(snapshotMap); + } + + public HashMap toEncodedDictionary(ObvBackupAndSyncDelegate... delegates) { + try { + HashMap map = new HashMap<>(); + for (ObvBackupAndSyncDelegate delegate : delegates) { + ObvSyncSnapshotNode node = snapshotMap.get(delegate.getTag()); + if (node == null) { + return null; + } + map.put(new DictionaryKey(delegate.getTag()), Encoded.of(delegate.serialize(node))); + } + return map; + } catch (Exception e) { + return null; + } + } + + + public static ObvSyncSnapshot fromEncodedDictionary(HashMap map, ObvBackupAndSyncDelegate... delegates) { + try { + HashMap snapshotMap = new HashMap<>(); + for (ObvBackupAndSyncDelegate delegate : delegates) { + Encoded encodedNode = map.get(new DictionaryKey(delegate.getTag())); + if (encodedNode == null) { + return null; + } + snapshotMap.put(delegate.getTag(), delegate.deserialize(encodedNode.decodeBytes())); + } + return new ObvSyncSnapshot(snapshotMap); + } catch (Exception e) { + return null; + } + } + + public boolean areContentsTheSame(ObvSyncSnapshot otherSnapshot) { + if (otherSnapshot == null) { + return false; + } + if (!Objects.equals(snapshotMap.keySet(), otherSnapshot.snapshotMap.keySet())) { + return false; + } + for (Map.Entry entry : snapshotMap.entrySet()) { + if (!entry.getValue().areContentsTheSame(otherSnapshot.snapshotMap.get(entry.getKey()))) { + return false; + } + } + return true; + } + + public List computeDiff(ObvSyncSnapshot otherSnapshot) throws Exception { + if (otherSnapshot == null || !Objects.equals(snapshotMap.keySet(), otherSnapshot.snapshotMap.keySet())) { + throw new Exception(); + } + + List diffs = new ArrayList<>(); + for (Map.Entry entry : snapshotMap.entrySet()) { + diffs.addAll(entry.getValue().computeDiff(otherSnapshot.snapshotMap.get(entry.getKey()))); + } + + return diffs; + } +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvSyncSnapshotNode.java b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvSyncSnapshotNode.java new file mode 100644 index 00000000..e77746ce --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/engine/types/sync/ObvSyncSnapshotNode.java @@ -0,0 +1,31 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.engine.types.sync; + +import java.util.List; + +public interface ObvSyncSnapshotNode { + // returns true if both ObvSyncSnapshotNode are exactly the same (deep compare) + boolean areContentsTheSame(ObvSyncSnapshotNode otherSnapshotNode); + + // computes a list of differences between two snapshots + List computeDiff(ObvSyncSnapshotNode otherSnapshotNode) throws Exception; + +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/identity/IdentityManager.java b/obv_engine/engine/src/main/java/io/olvid/engine/identity/IdentityManager.java index 867f348a..7c2a756e 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/identity/IdentityManager.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/identity/IdentityManager.java @@ -49,7 +49,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.UUID; import io.olvid.engine.Logger; import io.olvid.engine.crypto.Hash; @@ -91,10 +90,14 @@ import io.olvid.engine.engine.types.JsonKeycloakRevocation; import io.olvid.engine.engine.types.JsonKeycloakUserDetails; import io.olvid.engine.engine.types.ObvCapability; +import io.olvid.engine.engine.types.sync.ObvBackupAndSyncDelegate; +import io.olvid.engine.engine.types.sync.ObvSyncAtom; import io.olvid.engine.engine.types.identities.ObvContactActiveOrInactiveReason; import io.olvid.engine.engine.types.identities.ObvGroupV2; import io.olvid.engine.engine.types.identities.ObvIdentity; import io.olvid.engine.engine.types.identities.ObvKeycloakState; +import io.olvid.engine.engine.types.identities.ObvOwnedDevice; +import io.olvid.engine.engine.types.sync.ObvSyncSnapshotNode; import io.olvid.engine.identity.databases.ContactDevice; import io.olvid.engine.identity.databases.ContactGroup; import io.olvid.engine.identity.databases.ContactGroupDetails; @@ -113,6 +116,7 @@ import io.olvid.engine.identity.databases.OwnedIdentityDetails; import io.olvid.engine.identity.databases.PendingGroupMember; import io.olvid.engine.identity.databases.ServerUserData; +import io.olvid.engine.identity.databases.sync.IdentityManagerSyncSnapshot; import io.olvid.engine.identity.datatypes.IdentityManagerSession; import io.olvid.engine.identity.datatypes.IdentityManagerSessionFactory; import io.olvid.engine.identity.datatypes.KeycloakGroupBlob; @@ -130,7 +134,7 @@ import io.olvid.engine.metamanager.SolveChallengeDelegate; import io.olvid.engine.protocol.datatypes.ProtocolStarterDelegate; -public class IdentityManager implements IdentityDelegate, SolveChallengeDelegate, EncryptionForIdentityDelegate, IdentityManagerSessionFactory, ObvManager { +public class IdentityManager implements IdentityDelegate, SolveChallengeDelegate, EncryptionForIdentityDelegate, ObvBackupAndSyncDelegate, IdentityManagerSessionFactory, ObvManager { private final String engineBaseDirectory; private final ObjectMapper jsonObjectMapper; private final SessionCommitListener backupNeededSessionCommitListener; @@ -167,6 +171,13 @@ public void initialisationComplete() { userInfo.put(IdentityNotifications.NOTIFICATION_OWNED_IDENTITY_CHANGED_ACTIVE_STATUS_OWNED_IDENTITY_KEY, ownedIdentity.getOwnedIdentity()); userInfo.put(IdentityNotifications.NOTIFICATION_OWNED_IDENTITY_CHANGED_ACTIVE_STATUS_ACTIVE_KEY, false); identityManagerSession.notificationPostingDelegate.postNotification(IdentityNotifications.NOTIFICATION_OWNED_IDENTITY_CHANGED_ACTIVE_STATUS, userInfo); + } else { + try { + // do an OwnedDeviceDiscoveryProtocol at every startup + protocolStarterDelegate.startOwnedDeviceDiscoveryProtocol(ownedIdentity.getOwnedIdentity()); + } catch (Exception e) { + e.printStackTrace(); + } } } } catch (SQLException e) { @@ -177,10 +188,12 @@ public void initialisationComplete() { try (IdentityManagerSession identityManagerSession = getSession()) { ContactIdentity[] contactIdentities = ContactIdentity.getAllActiveWithoutDevices(identityManagerSession); if (contactIdentities.length > 0) { - // TODO: do not do this too often Logger.i("Found " + contactIdentities.length + " contacts with no device. Starting corresponding deviceDiscoveryProtocols."); for (ContactIdentity contactIdentity : contactIdentities) { - protocolStarterDelegate.startDeviceDiscoveryProtocol(contactIdentity.getOwnedIdentity(), contactIdentity.getContactIdentity()); + if (contactIdentity.getLastNoDeviceContactDeviceDiscovery() < System.currentTimeMillis() - Constants.NO_DEVICE_CONTACT_DEVICE_DISCOVERY_INTERVAL) { + contactIdentity.setLastNoDeviceContactDeviceDiscovery(System.currentTimeMillis()); + protocolStarterDelegate.startDeviceDiscoveryProtocolWithinTransaction(identityManagerSession.session, contactIdentity.getOwnedIdentity(), contactIdentity.getContactIdentity()); + } } } } catch (Exception e) { @@ -411,7 +424,7 @@ public ObjectMapper getJsonObjectMapper() { } public void downloadAllUserData(Session session) throws Exception { - List ownedIdentityDetailsList = OwnedIdentityDetails.getAllWithMissinPhotoUrl(wrapSession(session)); + List ownedIdentityDetailsList = OwnedIdentityDetails.getAllWithMissingPhotoUrl(wrapSession(session)); for (OwnedIdentityDetails ownedIdentityDetails : ownedIdentityDetailsList) { protocolStarterDelegate.startDownloadIdentityPhotoProtocolWithinTransaction(session, ownedIdentityDetails.getOwnedIdentity(), ownedIdentityDetails.getOwnedIdentity(), ownedIdentityDetails.getJsonIdentityDetailsWithVersionAndPhoto()); } @@ -451,20 +464,19 @@ public byte[] solveChallenge(byte[] challenge, Identity identity, PRNGService pr } } - @Override - public UUID getApiKey(Identity identity) { - try (IdentityManagerSession identityManagerSession = getSession()) { - OwnedIdentity ownedIdentity = OwnedIdentity.get(identityManagerSession, identity); - if (ownedIdentity != null) { - return ownedIdentity.getApiKey(); - } else { - return null; - } - } catch (SQLException e) { - e.printStackTrace(); - return null; - } - } +// public UUID getApiKey(Identity identity) { +// try (IdentityManagerSession identityManagerSession = getSession()) { +// OwnedIdentity ownedIdentity = OwnedIdentity.get(identityManagerSession, identity); +// if (ownedIdentity != null) { +// return ownedIdentity.getApiKey(); +// } else { +// return null; +// } +// } catch (SQLException e) { +// e.printStackTrace(); +// return null; +// } +// } // endregion @@ -485,11 +497,11 @@ public boolean isActiveOwnedIdentity(Session session, Identity ownedIdentity) th } @Override - public Identity generateOwnedIdentity(Session session, String server, JsonIdentityDetails jsonIdentityDetails, UUID apiKey, ObvKeycloakState keycloakState, PRNGService prng) throws SQLException { + public Identity generateOwnedIdentity(Session session, String server, JsonIdentityDetails jsonIdentityDetails, ObvKeycloakState keycloakState, String deviceDisplayName, PRNGService prng) throws SQLException { if (!session.isInTransaction()) { session.startTransaction(); } - OwnedIdentity ownedIdentity = OwnedIdentity.create(wrapSession(session), server, null, null, jsonIdentityDetails, apiKey, prng); + OwnedIdentity ownedIdentity = OwnedIdentity.create(wrapSession(session), server, null, null, jsonIdentityDetails, deviceDisplayName, prng); if (ownedIdentity == null) { return null; } @@ -600,6 +612,12 @@ public void setOwnedIdentityDetailsServerLabelAndKey(Session session, Identity o } } + public void createOwnedIdentityServerUserData(Session session, Identity ownedIdentity, UID photoServerLabel) throws SQLException { + if (ServerUserData.createForOwnedIdentityDetails(wrapSession(session), ownedIdentity, photoServerLabel) == null) { + throw new SQLException(); + } + } + @Override public int publishLatestIdentityDetails(Session session, Identity ownedIdentity) throws SQLException { OwnedIdentity ownedIdentityObject = OwnedIdentity.get(wrapSession(session), ownedIdentity); @@ -619,6 +637,15 @@ public void discardLatestIdentityDetails(Session session, Identity ownedIdentity } } + @Override + public boolean setOwnedIdentityDetailsFromOtherDevice(Session session, Identity ownedIdentity, JsonIdentityDetailsWithVersionAndPhoto ownDetailsWithVersionAndPhoto) throws SQLException { + OwnedIdentity ownedIdentityObject = OwnedIdentity.get(wrapSession(session), ownedIdentity); + if (ownedIdentityObject != null) { + return ownedIdentityObject.setOwnedIdentityDetailsFromOtherDevice(ownDetailsWithVersionAndPhoto); + } + return false; + } + @Override public String getSerializedPublishedDetailsOfOwnedIdentity(Session session, Identity ownedIdentity) { return OwnedIdentity.getSerializedPublishedDetails(wrapSession(session), ownedIdentity); @@ -920,6 +947,14 @@ public void saveKeycloakJwks(Session session, Identity ownedIdentity, String ser } } + @Override + public void saveKeycloakApiKey(Session session, Identity ownedIdentity, String apiKey) throws SQLException { + OwnedIdentity ownedIdentityObject = OwnedIdentity.get(wrapSession(session), ownedIdentity); + if (ownedIdentityObject != null && ownedIdentityObject.isKeycloakManaged()) { + KeycloakServer.saveApiKey(wrapSession(session), ownedIdentityObject.getKeycloakServerUrl(), ownedIdentity, apiKey); + } + } + @Override public String getOwnedIdentityKeycloakUserId(Session session, Identity ownedIdentity) throws SQLException { OwnedIdentity ownedIdentityObject = OwnedIdentity.get(wrapSession(session), ownedIdentity); @@ -1024,13 +1059,6 @@ public JsonIdentityDetailsWithVersionAndPhoto[] getOwnedIdentityPublishedAndLate } - @Override - public void updateApiKeyOfOwnedIdentity(Session session, Identity ownedIdentity, UUID newApiKey) throws SQLException { - OwnedIdentity ownedIdentityObject = OwnedIdentity.get(wrapSession(session), ownedIdentity); - ownedIdentityObject.setApiKey(newApiKey); - session.addSessionCommitListener(backupNeededSessionCommitListener); - } - @Override public boolean updateKeycloakPushTopicsIfNeeded(Session session, Identity ownedIdentity, String serverUrl, List pushTopics) throws SQLException { KeycloakServer keycloakServer = KeycloakServer.get(wrapSession(session), serverUrl, ownedIdentity); @@ -1197,8 +1225,18 @@ public void reactivateOwnedIdentityIfNeeded(Session session, Identity ownedIdent if (ownedIdentityObject != null && !ownedIdentityObject.isActive()) { ownedIdentityObject.setActive(true); //////////// - // After reactivating an identity, we must recreate all channels (that were destroyed after the deactivation + // After reactivating an identity, we must recreate all channels (that were destroyed after the deactivation) + // - restart channel creation for all owned devices (those were not deleted during deactivation) // - restart all device discovery protocols + // - also do an owned device discovery + try { + for (UID ownedDeviceUid : getOtherDeviceUidsOfOwnedIdentity(session, ownedIdentity)) { + protocolStarterDelegate.startChannelCreationProtocolWithOwnedDevice(session, ownedIdentity, ownedDeviceUid); + } + } catch (Exception e) { + e.printStackTrace(); + } + ContactIdentity[] contactIdentities = ContactIdentity.getAll(wrapSession(session), ownedIdentity); for (ContactIdentity contactIdentity : contactIdentities) { try { @@ -1207,6 +1245,12 @@ public void reactivateOwnedIdentityIfNeeded(Session session, Identity ownedIdent e.printStackTrace(); } } + try { + protocolStarterDelegate.startOwnedDeviceDiscoveryProtocolWithinTransaction(session, ownedIdentity); + } catch (Exception e) { + e.printStackTrace(); + } + } } @@ -1215,12 +1259,26 @@ public void deactivateOwnedIdentity(Session session, Identity ownedIdentity) thr OwnedIdentity ownedIdentityObject = OwnedIdentity.get(wrapSession(session), ownedIdentity); // set inactive even if it is already deactivated to trigger the notification ownedIdentityObject.setActive(false); + // also clear any timestamp in the current device + OwnedDevice ownedDevice = OwnedDevice.getCurrentDeviceOfOwnedIdentity(wrapSession(session), ownedIdentity); + ownedDevice.setTimestamps(null, null); //////////// // After deactivating an identity, we must delete all channels // - clear all contact deviceUid // - delete all channels + // - we keep our owned devices, so that the app still knows the list + // - we trigger all ongoing sync protocols so that they detect the channel is gone and can finish ContactDevice.deleteAll(wrapSession(session), ownedIdentity); channelDelegate.deleteAllChannelsForOwnedIdentity(session, ownedIdentity); +// protocolStarterDelegate.triggerOwnedDevicesSync(session, ownedIdentity); + } + + @Override + public void markOwnedIdentityForDeletion(Session session, Identity ownedIdentity) throws SQLException { + OwnedIdentity ownedIdentityObject = OwnedIdentity.get(wrapSession(session), ownedIdentity); + if (ownedIdentityObject != null) { + ownedIdentityObject.markForDeletion(); + } } // endregion @@ -1246,7 +1304,6 @@ public UID[] getOtherDeviceUidsOfOwnedIdentity(Session session, Identity ownedId } @Override - // Either returns the currentDeviceUid of an ObvOwnedIdentity or the deviceUid of an OwnedEphemeralIdentity public UID getCurrentDeviceUidOfOwnedIdentity(Session session, Identity ownedIdentity) throws SQLException { OwnedIdentity ownedIdentityObject = OwnedIdentity.get(wrapSession(session), ownedIdentity); if (ownedIdentityObject != null) { @@ -1256,27 +1313,77 @@ public UID getCurrentDeviceUidOfOwnedIdentity(Session session, Identity ownedIde } @Override - public Identity getOwnedIdentityForDeviceUid(Session session, UID currentDeviceUid) throws SQLException { + public Identity getOwnedIdentityForCurrentDeviceUid(Session session, UID currentDeviceUid) throws SQLException { OwnedDevice ownedDevice = OwnedDevice.get(wrapSession(session), currentDeviceUid); - if (ownedDevice != null) { + if (ownedDevice != null && ownedDevice.isCurrentDevice()) { return ownedDevice.getOwnedIdentity(); } return null; } @Override - public void addDeviceForOwnedIdentity(Session session, UID deviceUid, Identity ownedIdentity) throws SQLException { - // This returns null (FOREIGN KEY CONSTRAINT FAILED) if ownedIdentity is not an OwnedIdentity - OwnedDevice ownedDevice = OwnedDevice.createOtherDevice(wrapSession(session), deviceUid, ownedIdentity); - if (ownedDevice == null) { + public void addDeviceForOwnedIdentity(Session session, Identity ownedIdentity, UID deviceUid, String displayName, Long expirationTimestamp, Long lastRegistrationTimestamp, boolean channelCreationAlreadyInProgress) throws SQLException { + // check if the device already exists first + OwnedDevice ownedDevice = OwnedDevice.get(wrapSession(session), deviceUid); + if (ownedDevice != null && !Objects.equals(ownedDevice.getOwnedIdentity(), ownedIdentity)) { + Logger.e("Error: trying to addDeviceForOwnedIdentity for a deviceUid already used by another identity"); throw new SQLException(); } + // only create the device if it does not already exist + if (ownedDevice == null) { + ownedDevice = OwnedDevice.createOtherDevice(wrapSession(session), deviceUid, ownedIdentity, displayName, expirationTimestamp, lastRegistrationTimestamp, channelCreationAlreadyInProgress); + if (ownedDevice == null) { + throw new SQLException(); + } + } } @Override - public boolean isRemoteDeviceUidOfOwnedIdentity(Session session, UID deviceUid, Identity ownedIdentity) throws SQLException { + public void updateOwnedDevice(Session session, Identity ownedIdentity, UID deviceUid, String displayName, Long expirationTimestamp, Long lastRegistrationTimestamp) throws SQLException { + // check that the device exists and is for the right ownedIdentity OwnedDevice ownedDevice = OwnedDevice.get(wrapSession(session), deviceUid); - return (ownedDevice != null) && !ownedDevice.isCurrentDevice(); + if (ownedDevice != null && Objects.equals(ownedDevice.getOwnedIdentity(), ownedIdentity)) { + if (!Objects.equals(displayName, ownedDevice.getDisplayName())) { + ownedDevice.setDisplayName(displayName); + } + if (!Objects.equals(expirationTimestamp, ownedDevice.getExpirationTimestamp()) + || !Objects.equals(lastRegistrationTimestamp, ownedDevice.getLastRegistrationTimestamp())) { + ownedDevice.setTimestamps(expirationTimestamp, lastRegistrationTimestamp); + } + } + } + + @Override + public void removeDeviceForOwnedIdentity(Session session, Identity ownedIdentity, UID deviceUid) throws SQLException { + OwnedDevice ownedDevice = OwnedDevice.get(wrapSession(session), deviceUid); + if (ownedDevice != null && ownedDevice.getOwnedIdentity().equals(ownedIdentity)) { + ownedDevice.delete(); + } + } + + + @Override + public List getDevicesOfOwnedIdentity(Session session, Identity ownedIdentity) throws SQLException { + List ownedDevices = OwnedDevice.getAllDevicesOfIdentity(wrapSession(session), ownedIdentity); + List obvOwnedDevices = new ArrayList<>(); + for (OwnedDevice ownedDevice : ownedDevices) { + obvOwnedDevices.add(new ObvOwnedDevice(ownedDevice.getOwnedIdentity().getBytes(), + ownedDevice.getUid().getBytes(), + new ObvOwnedDevice.ServerDeviceInfo(ownedDevice.getDisplayName(), ownedDevice.getExpirationTimestamp(), ownedDevice.getLastRegistrationTimestamp()), + ownedDevice.isCurrentDevice(), + channelDelegate.checkIfObliviousChannelIsConfirmed(session, ownedIdentity, ownedDevice.getUid(), ownedIdentity) + )); + } + return obvOwnedDevices; + } + + @Override + public String getCurrentDeviceDisplayName(Session session, Identity ownedIdentity) throws SQLException { + OwnedDevice device = OwnedDevice.getCurrentDeviceOfOwnedIdentity(wrapSession(session), ownedIdentity); + if (device != null) { + return device.getDisplayName(); + } + return null; } // endregion @@ -1351,12 +1458,14 @@ public Identity[] getContactsOfOwnedIdentity(Session session, Identity ownedIden @Override - public void trustPublishedContactDetails(Session session, Identity contactIdentity, Identity ownedIdentity) throws SQLException { + public JsonIdentityDetailsWithVersionAndPhoto trustPublishedContactDetails(Session session, Identity contactIdentity, Identity ownedIdentity) throws SQLException { ContactIdentity contactIdentityObject = ContactIdentity.get(wrapSession(session), ownedIdentity, contactIdentity); if (contactIdentityObject != null) { - contactIdentityObject.trustPublishedDetails(); + JsonIdentityDetailsWithVersionAndPhoto details = contactIdentityObject.trustPublishedDetails(); session.addSessionCommitListener(backupNeededSessionCommitListener); + return details; } + return null; } @Override @@ -1594,13 +1703,13 @@ public boolean reBlockForcefullyUnblockedContact(Session session, Identity owned @Override - public boolean addDeviceForContactIdentity(Session session, Identity ownedIdentity, Identity contactIdentity, UID deviceUid) throws SQLException { + public boolean addDeviceForContactIdentity(Session session, Identity ownedIdentity, Identity contactIdentity, UID deviceUid, boolean channelCreationAlreadyInProgress) throws SQLException { ContactIdentity contact = ContactIdentity.get(wrapSession(session), ownedIdentity, contactIdentity); if (contact != null && contact.isActive()) { ContactDevice contactDevice = ContactDevice.get(wrapSession(session), deviceUid, contactIdentity, ownedIdentity); // only create the contact device if it does not already exist if (contactDevice == null) { - contactDevice = ContactDevice.create(wrapSession(session), deviceUid, contactIdentity, ownedIdentity); + contactDevice = ContactDevice.create(wrapSession(session), deviceUid, contactIdentity, ownedIdentity, channelCreationAlreadyInProgress); if (contactDevice == null) { throw new SQLException(); } @@ -1939,7 +2048,7 @@ public byte[] signGroupInvitationNonce(Session session, Constants.SignatureConte // region groups @Override - public void createContactGroup(Session session, Identity ownedIdentity, GroupInformation groupInformation, Identity[] groupMembers, IdentityWithSerializedDetails[] pendingGroupMembers) throws Exception { + public void createContactGroup(Session session, Identity ownedIdentity, GroupInformation groupInformation, Identity[] groupMembers, IdentityWithSerializedDetails[] pendingGroupMembers, boolean createdByMeOnOtherDevice) throws Exception { // check that all members are indeed existing contacts for (Identity groupMember: groupMembers) { if (!isIdentityAContactOfOwnedIdentity(session, ownedIdentity, groupMember)) { @@ -1954,7 +2063,8 @@ public void createContactGroup(Session session, Identity ownedIdentity, GroupInf groupInformation.getGroupOwnerAndUid(), ownedIdentity, groupInformation.serializedGroupDetailsWithVersionAndPhoto, - groupInformation.groupOwnerIdentity.equals(ownedIdentity) ? null : groupInformation.groupOwnerIdentity + groupInformation.groupOwnerIdentity.equals(ownedIdentity) ? null : groupInformation.groupOwnerIdentity, + createdByMeOnOtherDevice ); for (Identity groupMember: groupMembers) { ContactGroupMembersJoin.create(identityManagerSession, groupInformation.getGroupOwnerAndUid(), ownedIdentity, groupMember); @@ -2205,11 +2315,7 @@ public void updateGroupMembersAndDetails(Session session, Identity ownedIdentity throw new Exception(); } - // this method should only be called for groups you do not own - if (ownedIdentity.equals(groupInformation.groupOwnerIdentity)) { - Logger.w("Error: in updateGroupMembersAndDetails, group is owned"); - throw new Exception(); - } + boolean iAmTheGroupOwner = ownedIdentity.equals(groupInformation.groupOwnerIdentity); ContactGroup contactGroup = ContactGroup.get(wrapSession(session), groupInformation.getGroupOwnerAndUid(), ownedIdentity); if (contactGroup == null) { @@ -2220,6 +2326,10 @@ public void updateGroupMembersAndDetails(Session session, Identity ownedIdentity // first, update the details (if needed) JsonGroupDetailsWithVersionAndPhoto jsonGroupDetailsWithVersionAndPhoto = jsonObjectMapper.readValue(groupInformation.serializedGroupDetailsWithVersionAndPhoto, JsonGroupDetailsWithVersionAndPhoto.class); if (contactGroup.updatePublishedDetails(jsonGroupDetailsWithVersionAndPhoto, false)) { + if (iAmTheGroupOwner) { + // If I updated the group, auto-trust new details + contactGroup.trustPublishedDetails(); + } session.addSessionCommitListener(backupNeededSessionCommitListener); } @@ -2258,8 +2368,14 @@ public void updateGroupMembersAndDetails(Session session, Identity ownedIdentity // create contact if it does not exist ContactIdentity contactIdentityObject = ContactIdentity.get(wrapSession(session), ownedIdentity, groupMember.identity); if (contactIdentityObject == null) { - addContactIdentity(session, groupMember.identity, groupMember.serializedDetails, ownedIdentity, TrustOrigin.createGroupTrustOrigin(System.currentTimeMillis(), groupInformation.groupOwnerIdentity), false); - } else { + if (ownedIdentity.equals(groupInformation.groupOwnerIdentity)) { + // We are forced to create a contact without a contact origin + // --> this is not good, but we don't have a choice. A group was created/updated on another device but we do not know this contact yet... + addContactIdentity(session, groupMember.identity, groupMember.serializedDetails, ownedIdentity, null, false); + } else { + addContactIdentity(session, groupMember.identity, groupMember.serializedDetails, ownedIdentity, TrustOrigin.createGroupTrustOrigin(System.currentTimeMillis(), groupInformation.groupOwnerIdentity), false); + } + } else if (!ownedIdentity.equals(groupInformation.groupOwnerIdentity)) { addTrustOriginToContact(session, groupMember.identity, ownedIdentity, TrustOrigin.createGroupTrustOrigin(System.currentTimeMillis(), groupInformation.groupOwnerIdentity), false); } @@ -2441,12 +2557,14 @@ public String getGroupPhotoUrl(Session session, Identity ownedIdentity, byte[] g } @Override - public void trustPublishedGroupDetails(Session session, Identity ownedIdentity, byte[] groupOwnerAndUid) throws SQLException { + public JsonGroupDetailsWithVersionAndPhoto trustPublishedGroupDetails(Session session, Identity ownedIdentity, byte[] groupOwnerAndUid) throws SQLException { ContactGroup contactGroup = ContactGroup.get(wrapSession(session), groupOwnerAndUid, ownedIdentity); if (contactGroup != null) { - contactGroup.trustPublishedDetails(); + JsonGroupDetailsWithVersionAndPhoto details = contactGroup.trustPublishedDetails(); session.addSessionCommitListener(backupNeededSessionCommitListener); + return details; } + return null; } @Override @@ -2470,6 +2588,12 @@ public void setOwnedGroupDetailsServerLabelAndKey(Session session, Identity owne } } + public void createGroupV1ServerUserData(Session session, Identity ownedIdentity, UID photoServerLabel, byte[] groupOwnerAndUid) throws SQLException { + if (ServerUserData.createForOwnedGroupDetails(wrapSession(session), ownedIdentity, photoServerLabel, groupOwnerAndUid) == null) { + throw new SQLException(); + } + } + @Override public void updateOwnedGroupPhoto(Session session, Identity ownedIdentity, byte[] groupOwnerAndUid, String absolutePhotoUrl, boolean partOfGroupCreation) throws Exception { ContactGroup group = ContactGroup.get(wrapSession(session), groupOwnerAndUid, ownedIdentity); @@ -2578,7 +2702,7 @@ public void pushMembersOfOwnedGroupsToContact(UID currentDeviceUid, Identity con // region Groups v2 @Override - public void createNewGroupV2(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier, String serializedGroupDetails, String absolutePhotoUrl, GroupV2.ServerPhotoInfo serverPhotoInfo, byte[] verifiedAdministratorsChain, GroupV2.BlobKeys blobKeys, byte[] ownGroupInvitationNonce, List ownPermissionStrings, HashSet otherGroupMembers) throws Exception { + public void createNewGroupV2(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier, String serializedGroupDetails, String absolutePhotoUrl, GroupV2.ServerPhotoInfo serverPhotoInfo, byte[] verifiedAdministratorsChain, GroupV2.BlobKeys blobKeys, byte[] ownGroupInvitationNonce, List ownPermissionStrings, HashSet otherGroupMembers, String serializedGroupType) throws Exception { if (!ownPermissionStrings.contains(GroupV2.Permission.GROUP_ADMIN.getString())) { Logger.e("Error in createNewContactGroupV2: ownPermissions do not contain GROUP_ADMIN."); throw new Exception(); @@ -2605,7 +2729,8 @@ public void createNewGroupV2(Session session, Identity ownedIdentity, GroupV2.Id verifiedAdministratorsChain, blobKeys, ownGroupInvitationNonce, - ownPermissionStrings + ownPermissionStrings, + serializedGroupType ); if (group == null) { throw new Exception("Unable to create ContactGroupV2"); @@ -2638,7 +2763,7 @@ public void createNewGroupV2(Session session, Identity ownedIdentity, GroupV2.Id } @Override - public boolean createJoinedGroupV2(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier, GroupV2.BlobKeys blobKeys, GroupV2.ServerBlob serverBlob) throws Exception { + public boolean createJoinedGroupV2(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier, GroupV2.BlobKeys blobKeys, GroupV2.ServerBlob serverBlob, boolean createdByMeOnOtherDevice) throws Exception { if ((ownedIdentity == null) || (groupIdentifier == null) || (groupIdentifier.category == GroupV2.Identifier.CATEGORY_KEYCLOAK) || (serverBlob == null)) { throw new Exception(); } @@ -2682,7 +2807,9 @@ public boolean createJoinedGroupV2(Session session, Identity ownedIdentity, Grou serverBlob.administratorsChain.encode().getBytes(), blobKeys, ownIdentityAndPermissionsAndDetails.groupInvitationNonce, - ownIdentityAndPermissionsAndDetails.permissionStrings + ownIdentityAndPermissionsAndDetails.permissionStrings, + serverBlob.serializedGroupType, + createdByMeOnOtherDevice ); if (group == null) { throw new Exception("Unable to create joined ContactGroupV2"); @@ -2776,6 +2903,18 @@ public Integer getGroupV2Version(Session session, Identity ownedIdentity, GroupV return groupV2.getVersion(); } + @Override + public String getGroupV2JsonGroupType(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws SQLException { + if ((ownedIdentity == null) || (groupIdentifier == null)) { + return null; + } + ContactGroupV2 groupV2 = ContactGroupV2.get(wrapSession(session), ownedIdentity, groupIdentifier); + if (groupV2 == null) { + return null; + } + return groupV2.getSerializedJsonGroupType(); + } + @Override public boolean isGroupV2Frozen(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws SQLException { if ((ownedIdentity == null) || (groupIdentifier == null) || (groupIdentifier.category == GroupV2.Identifier.CATEGORY_KEYCLOAK)) { @@ -2958,22 +3097,22 @@ private static ObvGroupV2 groupV2toObvGroupV2(IdentityManagerSession identityMan } @Override - public void trustGroupV2PublishedDetails(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws SQLException { + public int trustGroupV2PublishedDetails(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws SQLException { if ((ownedIdentity == null) || (groupIdentifier == null)) { - return; + return -1; } ContactGroupV2 groupV2 = ContactGroupV2.get(wrapSession(session), ownedIdentity, groupIdentifier); if (groupV2 == null) { - return; + return -1; } int trustedVersion = groupV2.getTrustedDetailsVersion(); if (trustedVersion != groupV2.getVersion()) { groupV2.setTrustedDetailsVersion(groupV2.getVersion()); ContactGroupV2Details.cleanup(wrapSession(session), ownedIdentity, groupIdentifier, groupV2.getVersion(), groupV2.getVersion()); } - session.addSessionCommitListener(backupNeededSessionCommitListener); + return groupV2.getVersion(); } // only for CATEGORY_SERVER groups. This is only used for UserData management @@ -3127,7 +3266,7 @@ public void initiateGroupV2BatchKeysResend(UID currentDeviceUid, Identity contac } try (IdentityManagerSession identityManagerSession = getSession()) { - Identity ownedIdentity = getOwnedIdentityForDeviceUid(identityManagerSession.session, currentDeviceUid); + Identity ownedIdentity = getOwnedIdentityForCurrentDeviceUid(identityManagerSession.session, currentDeviceUid); if (ownedIdentity == null) { return; } @@ -3399,7 +3538,7 @@ public void initiateBackup(final BackupDelegate backupDelegate, final String tag } @Override - public ObvIdentity[] restoreOwnedIdentitiesFromBackup(String serializedJsonPojo, PRNGService prng) { + public ObvIdentity[] restoreOwnedIdentitiesFromBackup(String serializedJsonPojo, String deviceDisplayName, PRNGService prng) { try (IdentityManagerSession identityManagerSession = getSession()) { //////////////// // If an ownedIdentity already exists, we abort @@ -3416,7 +3555,7 @@ public ObvIdentity[] restoreOwnedIdentitiesFromBackup(String serializedJsonPojo, identityManagerSession.session.startTransaction(); for (OwnedIdentity.Pojo_0 ownedIdentityPojo : ownedIdentityPojos) { - restoredIdentities.add(OwnedIdentity.restore(identityManagerSession, ownedIdentityPojo, prng)); + restoredIdentities.add(OwnedIdentity.restore(identityManagerSession, ownedIdentityPojo, deviceDisplayName, prng)); } identityManagerSession.session.commit(); @@ -3428,15 +3567,18 @@ public ObvIdentity[] restoreOwnedIdentitiesFromBackup(String serializedJsonPojo, } @Override - public void restoreContactsAndGroupsFromBackup(String serializedJsonPojo, Identity[] restoredIdentities, long backupTimestamp) { - Set restoredOwnedIdentities = new HashSet<>(Arrays.asList(restoredIdentities)); + public void restoreContactsAndGroupsFromBackup(String serializedJsonPojo, ObvIdentity[] restoredOwnedIdentities, long backupTimestamp) { + Set restoredIdentities = new HashSet<>(); + for (ObvIdentity obvOwnedIdentity : restoredOwnedIdentities) { + restoredIdentities.add(obvOwnedIdentity.getIdentity()); + } try (IdentityManagerSession identityManagerSession = getSession()) { OwnedIdentity.Pojo_0[] ownedIdentityPojos = jsonObjectMapper.readValue(serializedJsonPojo, new TypeReference<>() {}); for (OwnedIdentity.Pojo_0 ownedIdentityPojo : ownedIdentityPojos) { Identity ownedIdentity = Identity.of(ownedIdentityPojo.owned_identity); - if (!restoredOwnedIdentities.contains(ownedIdentity)) { + if (!restoredIdentities.contains(ownedIdentity)) { continue; } @@ -3444,6 +3586,13 @@ public void restoreContactsAndGroupsFromBackup(String serializedJsonPojo, Identi ContactGroup.restoreAllForOwner(identityManagerSession, ownedIdentity, ownedIdentity, ownedIdentityPojo.owned_groups, backupTimestamp); ContactGroupV2.restoreAll(identityManagerSession, protocolStarterDelegate, ownedIdentity, ownedIdentityPojo.groups_v2); } + + + for (ObvIdentity obvOwnedIdentity : restoredOwnedIdentities) { + if (obvOwnedIdentity.isActive()) { + reactivateOwnedIdentityIfNeeded(identityManagerSession.session, obvOwnedIdentity.getIdentity()); + } + } } catch (Exception e) { e.printStackTrace(); } @@ -3487,6 +3636,72 @@ public void updateUserDataNextRefreshTimestamp(Session session, Identity ownedId } } + // endregion + + // region Device sync + + @Override + public void processSyncItem(Session session, Identity ownedIdentity, ObvSyncAtom obvSyncAtom) throws Exception { + switch (obvSyncAtom.syncType) { + case ObvSyncAtom.TYPE_TRUST_CONTACT_DETAILS: { + try { + JsonIdentityDetailsWithVersionAndPhoto atomDetails = jsonObjectMapper.readValue(obvSyncAtom.getStringValue(), JsonIdentityDetailsWithVersionAndPhoto.class); + JsonIdentityDetailsWithVersionAndPhoto[] dbDetails = getContactPublishedAndTrustedDetails(session, ownedIdentity, obvSyncAtom.getContactIdentity()); + // check if there are indeed details to trust + if (dbDetails.length == 2) { + // check that the published details actually match those we received + if (Objects.equals(dbDetails[0].getPhotoServerKey() == null ? null : new Encoded(dbDetails[0].getPhotoServerKey()).decodeSymmetricKey(), + atomDetails.getPhotoServerKey() == null ? null : new Encoded(atomDetails.getPhotoServerKey()).decodeSymmetricKey()) + && Arrays.equals(dbDetails[0].getPhotoServerLabel(), atomDetails.getPhotoServerLabel()) + && Objects.equals(dbDetails[0].getIdentityDetails(), atomDetails.getIdentityDetails())) { + trustPublishedContactDetails(session, obvSyncAtom.getContactIdentity(), ownedIdentity); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + break; + } + case ObvSyncAtom.TYPE_TRUST_GROUP_V1_DETAILS: { + try { + JsonGroupDetailsWithVersionAndPhoto atomDetails = jsonObjectMapper.readValue(obvSyncAtom.getStringValue(), JsonGroupDetailsWithVersionAndPhoto.class); + JsonGroupDetailsWithVersionAndPhoto[] dbDetails = getGroupPublishedAndLatestOrTrustedDetails(session, ownedIdentity, obvSyncAtom.getBytesGroupOwnerAndUid()); + // check if there are indeed details to trust + if (dbDetails.length == 2) { + // check that the published details actually match those we received + if (Objects.equals(dbDetails[0].getPhotoServerKey() == null ? null : new Encoded(dbDetails[0].getPhotoServerKey()).decodeSymmetricKey(), + atomDetails.getPhotoServerKey() == null ? null : new Encoded(atomDetails.getPhotoServerKey()).decodeSymmetricKey()) + && Arrays.equals(dbDetails[0].getPhotoServerLabel(), atomDetails.getPhotoServerLabel()) + && Objects.equals(dbDetails[0].getGroupDetails(), atomDetails.getGroupDetails())) { + trustPublishedGroupDetails(session, ownedIdentity, obvSyncAtom.getBytesGroupOwnerAndUid()); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + break; + } + case ObvSyncAtom.TYPE_TRUST_GROUP_V2_DETAILS: { + try { + int version = obvSyncAtom.getIntegerValue(); + GroupV2.Identifier groupIdentifier = obvSyncAtom.getGroupIdentifier(); + ContactGroupV2 groupV2 = ContactGroupV2.get(wrapSession(session), ownedIdentity, groupIdentifier); + // check if there are indeed details to trust matching the version + if (groupV2 != null && groupV2.getVersion() != groupV2.getTrustedDetailsVersion() && groupV2.getVersion() == version) { + trustGroupV2PublishedDetails(session, ownedIdentity, groupIdentifier); + } + } catch (Exception e) { + e.printStackTrace(); + } + break; + } + default: { + throw new Exception("Unknown Identity Manager sync atom type"); + } + } + } + + // endregion // endregion @@ -3522,5 +3737,54 @@ public AuthEncKey unwrap(Session session, EncryptedBytes wrappedKey, Identity to } } + @Override + public byte[] decrypt(Session session, EncryptedBytes ciphertext, Identity toIdentity) throws SQLException { + try { + OwnedIdentity ownedIdentity = OwnedIdentity.get(wrapSession(session), toIdentity); + if (ownedIdentity == null) { + return null; + } + PrivateIdentity privateIdentity = ownedIdentity.getPrivateIdentity(); + PublicKeyEncryption pubEnc = Suite.getPublicKeyEncryption(privateIdentity.getEncryptionPublicKey()); + return pubEnc.decrypt(privateIdentity.getEncryptionPrivateKey(), ciphertext); + } catch (DecryptionException | InvalidKeyException e) { + return null; + } + } + + // endregion + + // region implement ObvBackupAndSyncDelegate + + @Override + public String getTag() { + return "identity"; + } + + @Override + public ObvSyncSnapshotNode getSyncSnapshot(Identity ownedIdentity) { + try (IdentityManagerSession identityManagerSession = getSession()) { + // start a transaction to be sure the db is not modified while the snapshot is being computed! + identityManagerSession.session.startTransaction(); + return IdentityManagerSyncSnapshot.of(identityManagerSession, ownedIdentity); + } catch (Exception e) { + return null; + } + } + + + @Override + public byte[] serialize(ObvSyncSnapshotNode snapshotNode) throws Exception { + if (!(snapshotNode instanceof IdentityManagerSyncSnapshot)) { + throw new Exception("IdentityBackupDelegate can only serialize IdentityManagerSyncSnapshot"); + } + return jsonObjectMapper.writeValueAsBytes(snapshotNode); + } + + @Override + public ObvSyncSnapshotNode deserialize(byte[] serializedSnapshotNode) throws Exception { + return jsonObjectMapper.readValue(serializedSnapshotNode, IdentityManagerSyncSnapshot.class); + } + // endregion } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/ContactDevice.java b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/ContactDevice.java index a6954c9a..86fac975 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/ContactDevice.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/ContactDevice.java @@ -73,13 +73,14 @@ public List getDeviceCapabilities() { return ObvCapability.deserializeDeviceCapabilities(serializedDeviceCapabilities); } - public static ContactDevice create(IdentityManagerSession identityManagerSession, UID uid, Identity contactIdentity, Identity ownedIdentity) { + public static ContactDevice create(IdentityManagerSession identityManagerSession, UID uid, Identity contactIdentity, Identity ownedIdentity, boolean channelCreationAlreadyInProgress) { if ((uid == null) || (contactIdentity == null) || (ownedIdentity == null)) { return null; } try { ContactDevice contactDevice = new ContactDevice(identityManagerSession, uid, contactIdentity, ownedIdentity, null); contactDevice.insert(); + contactDevice.channelCreationAlreadyInProgress = channelCreationAlreadyInProgress; return contactDevice; } catch (SQLException e) { return null; @@ -256,6 +257,7 @@ public boolean setRawDeviceCapabilities(String[] rawDeviceCapabilities) throws S // endregion + boolean channelCreationAlreadyInProgress = false; private long commitHookBits = 0; private static final long HOOK_BIT_INSERTED = 0x1; private static final long HOOK_BIT_CAPABILITIES_UPDATED = 0x2; @@ -267,6 +269,7 @@ public void wasCommitted() { userInfo.put(IdentityNotifications.NOTIFICATION_NEW_CONTACT_DEVICE_CONTACT_DEVICE_UID_KEY, uid); userInfo.put(IdentityNotifications.NOTIFICATION_NEW_CONTACT_DEVICE_OWNED_IDENTITY_KEY, ownedIdentity); userInfo.put(IdentityNotifications.NOTIFICATION_NEW_CONTACT_DEVICE_CONTACT_IDENTITY_KEY, contactIdentity); + userInfo.put(IdentityNotifications.NOTIFICATION_NEW_CONTACT_DEVICE_CHANNEL_CREATION_ALREADY_IN_PROGRESS_KEY, channelCreationAlreadyInProgress); identityManagerSession.notificationPostingDelegate.postNotification(IdentityNotifications.NOTIFICATION_NEW_CONTACT_DEVICE, userInfo); } if ((commitHookBits & HOOK_BIT_CAPABILITIES_UPDATED) != 0) { diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/ContactGroup.java b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/ContactGroup.java index 1eba814f..6962f579 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/ContactGroup.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/ContactGroup.java @@ -122,7 +122,7 @@ public GroupInformation getGroupInformation() { // update details of a group you do not own. Returns true if details were indeed updated public boolean updatePublishedDetails(JsonGroupDetailsWithVersionAndPhoto jsonGroupDetailsWithVersionAndPhoto, boolean allowDowngrade) throws Exception { - if (groupOwner == null || jsonGroupDetailsWithVersionAndPhoto == null) { + if (jsonGroupDetailsWithVersionAndPhoto == null) { return false; } final int newDetailsVersion = jsonGroupDetailsWithVersionAndPhoto.getVersion(); @@ -236,15 +236,18 @@ public boolean updatePublishedDetails(JsonGroupDetailsWithVersionAndPhoto jsonGr this.publishedDetailsVersion = newPublishedDetails.getVersion(); } } - commitHookBits |= HOOK_BIT_NEW_PUBLISHED_DETAILS; - identityManagerSession.session.addSessionCommitListener(this); + // no need to notify if I am the group owner processing a propagated message + if (groupOwner != null) { + commitHookBits |= HOOK_BIT_NEW_PUBLISHED_DETAILS; + identityManagerSession.session.addSessionCommitListener(this); + } return true; } // trust the details of a group you do not own - public void trustPublishedDetails() throws SQLException { - if (groupOwner == null || latestOrTrustedDetailsVersion == publishedDetailsVersion) { - return; + public JsonGroupDetailsWithVersionAndPhoto trustPublishedDetails() throws SQLException { + if (latestOrTrustedDetailsVersion == publishedDetailsVersion) { + return null; } try (PreparedStatement statement = identityManagerSession.session.prepareStatement("UPDATE " + TABLE_NAME + " SET " + LATEST_OR_TRUSTED_DETAILS_VERSION + " = ? " + @@ -259,6 +262,7 @@ public void trustPublishedDetails() throws SQLException { hookDetails = getLatestOrTrustedDetails().getJsonGroupDetailsWithVersionAndPhoto(); commitHookBits |= HOOK_BIT_PUBLISHED_DETAILS_TRUSTED; identityManagerSession.session.addSessionCommitListener(this); + return hookDetails; } // set details of a group you own @@ -426,7 +430,7 @@ public void discardLatestDetails() throws SQLException { } } - // usually for group you do not own, but can be for owned groups after a backup restore + // usually for group you do not own, but can be for owned groups after a backup restore or in multi-device public void setDetailsDownloadedPhotoUrl(int version, byte[] photo) throws Exception { ContactGroupDetails contactGroupDetails = ContactGroupDetails.get(identityManagerSession, groupOwnerAndUid, ownedIdentity, version); @@ -500,7 +504,7 @@ public void setGroupMembersVersion(long groupMembersVersion) throws Exception { // region constructors - public static ContactGroup create(IdentityManagerSession identityManagerSession, byte[] groupUid, Identity ownedIdentity, String serializedGroupDetailsWithVersionAndPhoto, Identity groupOwner) { + public static ContactGroup create(IdentityManagerSession identityManagerSession, byte[] groupUid, Identity ownedIdentity, String serializedGroupDetailsWithVersionAndPhoto, Identity groupOwner, boolean createdByMeOnOtherDevice) { if ((groupUid == null) || (serializedGroupDetailsWithVersionAndPhoto == null) || (ownedIdentity == null)) { return null; } @@ -524,6 +528,9 @@ public static ContactGroup create(IdentityManagerSession identityManagerSession, } ContactGroup contactGroup = new ContactGroup(identityManagerSession, groupUid, ownedIdentity, groupOwner, contactGroupDetails.getVersion()); contactGroup.insert(); + if (createdByMeOnOtherDevice) { + contactGroup.commitHookBits |= HOOK_BIT_CREATED_ON_OTHER_DEVICE; + } return contactGroup; } catch (SQLException e) { e.printStackTrace(); @@ -810,6 +817,7 @@ public static byte[][] getGroupOwnerAndUidsOfOwnedGroupsWithContact(IdentityMana private static final long HOOK_BIT_PHOTO_SET = 0x10; private static final long HOOK_BIT_DETAILS_PUBLISHED = 0x20; private static final long HOOK_BIT_SERVER_USER_DATA_CAN_BE_DELETED = 0x40; + private static final long HOOK_BIT_CREATED_ON_OTHER_DEVICE = 0x80; @Override public void wasCommitted() { @@ -817,6 +825,7 @@ public void wasCommitted() { HashMap userInfo = new HashMap<>(); userInfo.put(IdentityNotifications.NOTIFICATION_GROUP_CREATED_GROUP_OWNER_AND_UID_KEY, groupOwnerAndUid); userInfo.put(IdentityNotifications.NOTIFICATION_GROUP_CREATED_OWNED_IDENTITY_KEY, ownedIdentity); + userInfo.put(IdentityNotifications.NOTIFICATION_GROUP_CREATED_ON_OTHER_DEVICE_KEY, (commitHookBits & HOOK_BIT_CREATED_ON_OTHER_DEVICE) != 0); identityManagerSession.notificationPostingDelegate.postNotification(IdentityNotifications.NOTIFICATION_GROUP_CREATED, userInfo); } if ((commitHookBits & HOOK_BIT_DELETED) != 0) { diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/ContactGroupV2.java b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/ContactGroupV2.java index ab7ec9bc..811e9272 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/ContactGroupV2.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/ContactGroupV2.java @@ -85,7 +85,7 @@ public class ContactGroupV2 implements ObvDatabase { private byte[] verifiedAdministratorsChain; // null for a keycloak group static final String VERIFIED_ADMINISTRATORS_CHAIN = "verified_administrators_chain"; - private Seed blobMainSeed; // used to decrypt the blob on the server, null for a keycloak group + private Seed blobMainSeed; // used to decrypt the blob on the server, null for a keycloak group static final String BLOB_MAIN_SEED = "blob_main_seed"; private Seed blobVersionSeed; // used to decrypt the blob on the server, null for a keycloak group static final String BLOB_VERSION_SEED = "blob_version_seed"; @@ -101,6 +101,9 @@ public class ContactGroupV2 implements ObvDatabase { static final String PUSH_TOPIC = "push_topic"; private String serializedSharedSettings; // non-null only for keyclaok groups static final String SERIALIZED_SHARED_SETTINGS = "serialized_shared_settings"; + private String serializedJsonGroupType; + static final String SERIALIZED_JSON_GROUP_TYPE = "serialized_json_group_type"; + public Identity getOwnedIdentity() { return ownedIdentity; @@ -158,10 +161,14 @@ public String getSerializedSharedSettings() { return serializedSharedSettings; } + public String getSerializedJsonGroupType() { + return serializedJsonGroupType; + } + // region constructor // used only by the group creator to create a new group - public static ContactGroupV2 createNew(IdentityManagerSession identityManagerSession, Identity ownedIdentity, GroupV2.Identifier groupIdentifier, String serializedGroupDetails, String absolutePhotoUrl, GroupV2.ServerPhotoInfo serverPhotoInfo, byte[] verifiedAdministratorsChain, GroupV2.BlobKeys blobKeys, byte[] ownGroupInvitationNonce, List ownPermissionStrings) { + public static ContactGroupV2 createNew(IdentityManagerSession identityManagerSession, Identity ownedIdentity, GroupV2.Identifier groupIdentifier, String serializedGroupDetails, String absolutePhotoUrl, GroupV2.ServerPhotoInfo serverPhotoInfo, byte[] verifiedAdministratorsChain, GroupV2.BlobKeys blobKeys, byte[] ownGroupInvitationNonce, List ownPermissionStrings, String serializedGroupType) { if ((groupIdentifier == null) || (ownedIdentity == null) || (serializedGroupDetails == null) || (verifiedAdministratorsChain == null) || (blobKeys == null) || (ownGroupInvitationNonce == null)) { return null; } @@ -180,7 +187,7 @@ public static ContactGroupV2 createNew(IdentityManagerSession identityManagerSes byte[] serializedOwnPermissions = GroupV2.Permission.serializePermissionStrings(ownPermissionStrings); // when first creating the group, it is frozen. It will be unfrozen once the group is successfully uploaded to the server and the members can be notified - ContactGroupV2 contactGroup = new ContactGroupV2(identityManagerSession, groupIdentifier.groupUid, groupIdentifier.serverUrl, groupIdentifier.category, ownedIdentity, serializedOwnPermissions, contactGroupDetails.getVersion(), verifiedAdministratorsChain, blobKeys, ownGroupInvitationNonce, true, System.currentTimeMillis(), null, null); + ContactGroupV2 contactGroup = new ContactGroupV2(identityManagerSession, groupIdentifier.groupUid, groupIdentifier.serverUrl, groupIdentifier.category, ownedIdentity, serializedOwnPermissions, contactGroupDetails.getVersion(), verifiedAdministratorsChain, blobKeys, ownGroupInvitationNonce, true, System.currentTimeMillis(), null, null, serializedGroupType); contactGroup.insert(); contactGroup.commitHookBits |= HOOK_BIT_INSERTED_AS_NEW | HOOK_BIT_FROZEN_CHANGED; // this way the app also receives a frozen notification to mark the group as updating return contactGroup; @@ -191,7 +198,7 @@ public static ContactGroupV2 createNew(IdentityManagerSession identityManagerSes } - public static ContactGroupV2 createJoined(IdentityManagerSession identityManagerSession, Identity ownedIdentity, GroupV2.Identifier groupIdentifier, int version, String serializedGroupDetails, GroupV2.ServerPhotoInfo serverPhotoInfo, byte[] verifiedAdministratorsChain, GroupV2.BlobKeys blobKeys, byte[] ownGroupInvitationNonce, List ownPermissionStrings) { + public static ContactGroupV2 createJoined(IdentityManagerSession identityManagerSession, Identity ownedIdentity, GroupV2.Identifier groupIdentifier, int version, String serializedGroupDetails, GroupV2.ServerPhotoInfo serverPhotoInfo, byte[] verifiedAdministratorsChain, GroupV2.BlobKeys blobKeys, byte[] ownGroupInvitationNonce, List ownPermissionStrings, String serializedGroupType, boolean createdByMeOnOtherDevice) { if ((ownedIdentity == null) || (groupIdentifier == null) || (serializedGroupDetails == null) || (verifiedAdministratorsChain == null) || (blobKeys == null) || (ownGroupInvitationNonce == null) || (ownPermissionStrings == null)) { return null; } @@ -206,8 +213,11 @@ public static ContactGroupV2 createJoined(IdentityManagerSession identityManager return null; } - ContactGroupV2 contactGroup = new ContactGroupV2(identityManagerSession, groupIdentifier.groupUid, groupIdentifier.serverUrl, groupIdentifier.category, ownedIdentity, GroupV2.Permission.serializePermissionStrings(ownPermissionStrings), contactGroupDetails.getVersion(), verifiedAdministratorsChain, blobKeys, ownGroupInvitationNonce, false, System.currentTimeMillis(), null, null); + ContactGroupV2 contactGroup = new ContactGroupV2(identityManagerSession, groupIdentifier.groupUid, groupIdentifier.serverUrl, groupIdentifier.category, ownedIdentity, GroupV2.Permission.serializePermissionStrings(ownPermissionStrings), contactGroupDetails.getVersion(), verifiedAdministratorsChain, blobKeys, ownGroupInvitationNonce, false, System.currentTimeMillis(), null, null, serializedGroupType); contactGroup.insert(); + if (createdByMeOnOtherDevice) { + contactGroup.commitHookBits |= HOOK_BIT_INSERTED_AS_NEW | HOOK_BIT_CREATED_ON_OTHER_DEVICE; + } return contactGroup; } catch (Exception e) { e.printStackTrace(); @@ -231,7 +241,7 @@ public static ContactGroupV2 createKeycloak(IdentityManagerSession identityManag return null; } - ContactGroupV2 contactGroup = new ContactGroupV2(identityManagerSession, groupIdentifier.groupUid, groupIdentifier.serverUrl, groupIdentifier.category, ownedIdentity, GroupV2.Permission.serializePermissionStrings(ownPermissionStrings), contactGroupDetails.getVersion(), null, null, ownGroupInvitationNonce, false, lastModificationTimestamp, pushTopic, serializedSharedSettings); + ContactGroupV2 contactGroup = new ContactGroupV2(identityManagerSession, groupIdentifier.groupUid, groupIdentifier.serverUrl, groupIdentifier.category, ownedIdentity, GroupV2.Permission.serializePermissionStrings(ownPermissionStrings), contactGroupDetails.getVersion(), null, null, ownGroupInvitationNonce, false, lastModificationTimestamp, pushTopic, serializedSharedSettings, null); contactGroup.insert(); if (pushTopic != null) { contactGroup.commitHookBits |= HOOK_BIT_NEW_PUSH_TOPIC; @@ -244,7 +254,7 @@ public static ContactGroupV2 createKeycloak(IdentityManagerSession identityManag } } - private ContactGroupV2(IdentityManagerSession identityManagerSession, UID groupUid, String serverUrl, int category, Identity ownedIdentity, byte[] serializedOwnPermission, int version, byte[] verifiedAdministratorsChain, GroupV2.BlobKeys blobKeys, byte[] ownGroupInvitationNonce, boolean frozen, long lastModificationTimestamp, String pushTopic, String serializedSharedSettings) { + private ContactGroupV2(IdentityManagerSession identityManagerSession, UID groupUid, String serverUrl, int category, Identity ownedIdentity, byte[] serializedOwnPermission, int version, byte[] verifiedAdministratorsChain, GroupV2.BlobKeys blobKeys, byte[] ownGroupInvitationNonce, boolean frozen, long lastModificationTimestamp, String pushTopic, String serializedSharedSettings, String serializedJsonGroupType) { this.identityManagerSession = identityManagerSession; this.groupUid = groupUid; this.serverUrl = serverUrl; @@ -268,6 +278,7 @@ private ContactGroupV2(IdentityManagerSession identityManagerSession, UID groupU this.lastModificationTimestamp = lastModificationTimestamp; this.pushTopic = pushTopic; this.serializedSharedSettings = serializedSharedSettings; + this.serializedJsonGroupType = serializedJsonGroupType; } private ContactGroupV2(IdentityManagerSession identityManagerSession, ResultSet res) throws SQLException { @@ -303,6 +314,7 @@ private ContactGroupV2(IdentityManagerSession identityManagerSession, ResultSet this.lastModificationTimestamp = res.getLong(LAST_MODIFICATION_TIMESTAMP); this.pushTopic = res.getString(PUSH_TOPIC); this.serializedSharedSettings = res.getString(SERIALIZED_SHARED_SETTINGS); + this.serializedJsonGroupType = res.getString(SERIALIZED_JSON_GROUP_TYPE); } @@ -428,7 +440,7 @@ public static GroupV2.ServerBlob getServerBlob(IdentityManagerSession identityMa } } - return new GroupV2.ServerBlob(administratorsChain, groupMemberIdentityAndPermissionsAndDetailsList, group.version, serializedGroupDetails, serverPhotoInfo); + return new GroupV2.ServerBlob(administratorsChain, groupMemberIdentityAndPermissionsAndDetailsList, group.version, serializedGroupDetails, serverPhotoInfo, group.serializedJsonGroupType); } public static String getPhotoUrl(IdentityManagerSession identityManagerSession, Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws SQLException { @@ -849,6 +861,7 @@ public List updateWithNewBlob(GroupV2.ServerBlob serverBlob, GroupV2.B blobVersionSeed = blobKeys.blobVersionSeed; groupAdminServerAuthenticationPrivateKey = blobKeys.groupAdminServerAuthenticationPrivateKey; lastModificationTimestamp = System.currentTimeMillis(); + serializedJsonGroupType = serverBlob.serializedGroupType; // create the new group details GroupV2.Identifier groupIdentifier = getGroupIdentifier(); @@ -1348,6 +1361,7 @@ public static void createTable(Session session) throws SQLException { LAST_MODIFICATION_TIMESTAMP + " INTEGER NOT NULL, " + PUSH_TOPIC + " TEXT, " + SERIALIZED_SHARED_SETTINGS + " TEXT, " + + SERIALIZED_JSON_GROUP_TYPE + " TEXT, " + " CONSTRAINT PK_" + TABLE_NAME + " PRIMARY KEY(" + GROUP_UID + ", " + SERVER_URL + ", " + CATEGORY + ", " + OWNED_IDENTITY + "), " + " FOREIGN KEY (" + OWNED_IDENTITY + ") REFERENCES " + OwnedIdentity.TABLE_NAME + "(" + OwnedIdentity.OWNED_IDENTITY + ")," + " FOREIGN KEY (" + GROUP_UID + ", " + SERVER_URL + ", " + CATEGORY + ", " + OWNED_IDENTITY + ", " + VERSION + ") REFERENCES " + ContactGroupV2Details.TABLE_NAME + "(" + ContactGroupV2Details.GROUP_UID + ", " + ContactGroupV2Details.SERVER_URL + ", " + ContactGroupV2Details.CATEGORY + ", " + ContactGroupV2Details.OWNED_IDENTITY + ", " + ContactGroupV2Details.VERSION + ")," + @@ -1412,10 +1426,16 @@ public static void upgradeTable(Session session, int oldVersion, int newVersion) } oldVersion = 34; } + if (oldVersion < 35 && newVersion >= 35) { + try (Statement statement = session.createStatement()) { + Logger.d("MIGRATING `contact_group_v2` DATABASE FROM VERSION " + oldVersion + " to 35"); + statement.execute("ALTER TABLE contact_group_v2 ADD COLUMN `serialized_json_group_type` TEXT DEFAULT NULL"); + } + } } public void insert() throws SQLException { - try (PreparedStatement statement = identityManagerSession.session.prepareStatement("INSERT INTO " + TABLE_NAME + " VALUES (?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?);")) { + try (PreparedStatement statement = identityManagerSession.session.prepareStatement("INSERT INTO " + TABLE_NAME + " VALUES (?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?);")) { statement.setBytes(1, groupUid.getBytes()); statement.setString(2, serverUrl); statement.setInt(3, category); @@ -1434,6 +1454,7 @@ public void insert() throws SQLException { statement.setLong(14, lastModificationTimestamp); statement.setString(15, pushTopic); statement.setString(16, serializedSharedSettings); + statement.setString(17, serializedJsonGroupType); statement.executeUpdate(); commitHookBits |= HOOK_BIT_INSERTED; identityManagerSession.session.addSessionCommitListener(this); @@ -1486,7 +1507,8 @@ private void update() throws SQLException { FROZEN + " = ?, " + LAST_MODIFICATION_TIMESTAMP + " = ?, " + PUSH_TOPIC + " = ?, " + - SERIALIZED_SHARED_SETTINGS + " = ? " + + SERIALIZED_SHARED_SETTINGS + " = ?, " + + SERIALIZED_JSON_GROUP_TYPE + " = ? " + " WHERE " + GROUP_UID + " = ? " + " AND " + SERVER_URL + " = ? " + " AND " + CATEGORY + " = ? " + @@ -1506,10 +1528,12 @@ private void update() throws SQLException { statement.setString(11, pushTopic); statement.setString(12, serializedSharedSettings); - statement.setBytes(13, groupUid.getBytes()); - statement.setString(14, serverUrl); - statement.setInt(15, category); - statement.setBytes(16, ownedIdentity.getBytes()); + statement.setString(13, serializedJsonGroupType); + + statement.setBytes(14, groupUid.getBytes()); + statement.setString(15, serverUrl); + statement.setInt(16, category); + statement.setBytes(17, ownedIdentity.getBytes()); statement.executeUpdate(); } } @@ -1538,7 +1562,7 @@ public void triggerUpdateNotification() { private static final long HOOK_BIT_PHOTO_UPDATED = 0x20; private static final long HOOK_BIT_SERVER_USER_DATA_CAN_BE_DELETED = 0x40; private static final long HOOK_BIT_NEW_PUSH_TOPIC = 0x80; - + private static final long HOOK_BIT_CREATED_ON_OTHER_DEVICE = 0x100; @Override public void wasCommitted() { // nothing to do here @@ -1546,7 +1570,8 @@ public void wasCommitted() { HashMap userInfo = new HashMap<>(); userInfo.put(IdentityNotifications.NOTIFICATION_GROUP_V2_CREATED_OWNED_IDENTITY_KEY, ownedIdentity); userInfo.put(IdentityNotifications.NOTIFICATION_GROUP_V2_CREATED_GROUP_IDENTIFIER_KEY, getGroupIdentifier()); - userInfo.put(IdentityNotifications.NOTIFICATION_GROUP_V2_CREATED_NEW_GROUP_KEY, (commitHookBits & HOOK_BIT_INSERTED_AS_NEW) != 0); + userInfo.put(IdentityNotifications.NOTIFICATION_GROUP_V2_CREATED_CREATED_BY_ME_KEY, (commitHookBits & HOOK_BIT_INSERTED_AS_NEW) != 0); + userInfo.put(IdentityNotifications.NOTIFICATION_GROUP_V2_CREATED_ON_OTHER_DEVICE_KEY, (commitHookBits & HOOK_BIT_CREATED_ON_OTHER_DEVICE) != 0); identityManagerSession.notificationPostingDelegate.postNotification(IdentityNotifications.NOTIFICATION_GROUP_V2_CREATED, userInfo); } if ((commitHookBits & HOOK_BIT_DELETED) != 0) { @@ -1636,6 +1661,7 @@ Pojo_0 backup() throws SQLException { pojo.last_modification_timestamp = lastModificationTimestamp; pojo.push_topic = pushTopic; pojo.serialized_shared_settings = serializedSharedSettings; + pojo.serialized_json_group_type = serializedJsonGroupType; pojo.members = ContactGroupV2Member.backupAll(identityManagerSession, ownedIdentity, getGroupIdentifier()); pojo.pending_members = ContactGroupV2PendingMember.backupAll(identityManagerSession, ownedIdentity, getGroupIdentifier()); @@ -1687,7 +1713,8 @@ static void restore(IdentityManagerSession identityManagerSession, ProtocolStart false, pojo.last_modification_timestamp, pojo.push_topic, - pojo.serialized_shared_settings + pojo.serialized_shared_settings, + pojo.serialized_json_group_type ); groupV2.insert(); @@ -1722,6 +1749,7 @@ public static class Pojo_0 { public long last_modification_timestamp; public String push_topic; public String serialized_shared_settings; + public String serialized_json_group_type; public ContactGroupV2Member.Pojo_0[] members; public ContactGroupV2PendingMember.Pojo_0[] pending_members; diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/ContactIdentity.java b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/ContactIdentity.java index 7d006a86..fd61a678 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/ContactIdentity.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/ContactIdentity.java @@ -81,6 +81,8 @@ public class ContactIdentity implements ObvDatabase { static final String FORCEFULLY_TRUSTED_BY_USER = "forcefully_trusted_by_user"; private boolean oneToOne; static final String ONE_TO_ONE = "one_to_one"; + private long lastNoDeviceContactDeviceDiscovery; + static final String LAST_NO_DEVICE_CONTACT_DEVICE_DISCOVERY = "last_no_device_contact_device_discovery"; public Identity getContactIdentity() { return contactIdentity; @@ -122,6 +124,9 @@ public boolean isOneToOne() { return oneToOne; } + public long getLastNoDeviceContactDeviceDiscovery() { + return lastNoDeviceContactDeviceDiscovery; + } // region computed properties public UID[] getDeviceUids() throws SQLException { @@ -445,9 +450,9 @@ public static void unmarkAllCertifiedByOwnKeycloakContacts(IdentityManagerSessio } } - public void trustPublishedDetails() throws SQLException { + public JsonIdentityDetailsWithVersionAndPhoto trustPublishedDetails() throws SQLException { if (trustedDetailsVersion == publishedDetailsVersion) { - return; + return null; } try (PreparedStatement statement = identityManagerSession.session.prepareStatement("UPDATE " + TABLE_NAME + " SET " + TRUSTED_DETAILS_VERSION + " = ? " + @@ -462,6 +467,7 @@ public void trustPublishedDetails() throws SQLException { hookTrustedDetails = getTrustedDetails().getJsonIdentityDetailsWithVersionAndPhoto(); commitHookBits |= HOOK_BIT_PUBLISHED_DETAILS_TRUSTED; identityManagerSession.session.addSessionCommitListener(this); + return hookTrustedDetails; } public void setDetailsDownloadedPhotoUrl(int version, byte[] photo) throws Exception { @@ -539,6 +545,19 @@ private void setTrustLevel(TrustLevel trustLevel) throws SQLException { } } + public void setLastNoDeviceContactDeviceDiscovery(long lastNoDeviceContactDeviceDiscovery) throws SQLException { + try (PreparedStatement statement = identityManagerSession.session.prepareStatement("UPDATE " + TABLE_NAME + + " SET " + LAST_NO_DEVICE_CONTACT_DEVICE_DISCOVERY + " = ? " + + " WHERE " + CONTACT_IDENTITY + " = ? " + + " AND " + OWNED_IDENTITY + " = ?;")) { + statement.setLong(1, lastNoDeviceContactDeviceDiscovery); + statement.setBytes(2, contactIdentity.getBytes()); + statement.setBytes(3, ownedIdentity.getBytes()); + statement.executeUpdate(); + this.lastNoDeviceContactDeviceDiscovery = lastNoDeviceContactDeviceDiscovery; + } + } + // endregion // region constructors @@ -574,12 +593,16 @@ public static ContactIdentity create(IdentityManagerSession identityManagerSessi } } - ContactTrustOrigin contactTrustOrigin = ContactTrustOrigin.create(identityManagerSession, contactIdentity, ownedIdentity, trustOrigin); - if (contactTrustOrigin == null) { - Logger.e("Error create contactTrustOrigin in ContactIdentity.create()"); - throw new SQLException(); + if (trustOrigin != null) { + ContactTrustOrigin contactTrustOrigin = ContactTrustOrigin.create(identityManagerSession, contactIdentity, ownedIdentity, trustOrigin); + if (contactTrustOrigin == null) { + Logger.e("Error create contactTrustOrigin in ContactIdentity.create()"); + throw new SQLException(); + } + contactIdentityObject.setTrustLevel(contactTrustOrigin.getTrustLevel()); + } else { + contactIdentityObject.setTrustLevel(new TrustLevel(0, 0)); } - contactIdentityObject.setTrustLevel(contactTrustOrigin.getTrustLevel()); return contactIdentityObject; } catch (SQLException e) { return null; @@ -597,6 +620,7 @@ private ContactIdentity(IdentityManagerSession identityManagerSession, Identity this.revokedAsCompromised = false; this.forcefullyTrustedByUser = false; this.oneToOne = oneToOne; + this.lastNoDeviceContactDeviceDiscovery = 0; } private ContactIdentity(IdentityManagerSession identityManagerSession, ResultSet res) throws SQLException { @@ -614,6 +638,7 @@ private ContactIdentity(IdentityManagerSession identityManagerSession, ResultSet this.revokedAsCompromised = res.getBoolean(REVOKED_AS_COMPROMISED); this.forcefullyTrustedByUser = res.getBoolean(FORCEFULLY_TRUSTED_BY_USER); this.oneToOne = res.getBoolean(ONE_TO_ONE); + this.lastNoDeviceContactDeviceDiscovery = res.getLong(LAST_NO_DEVICE_CONTACT_DEVICE_DISCOVERY); } // endregion @@ -633,6 +658,7 @@ public static void createTable(Session session) throws SQLException { REVOKED_AS_COMPROMISED + " BIT NOT NULL, " + FORCEFULLY_TRUSTED_BY_USER + " BIT NOT NULL, " + ONE_TO_ONE + " BIT NOT NULL, " + + LAST_NO_DEVICE_CONTACT_DEVICE_DISCOVERY + " INTEGER NOT NULL, " + " CONSTRAINT PK_" + TABLE_NAME + " PRIMARY KEY(" + CONTACT_IDENTITY + ", " + OWNED_IDENTITY + "), " + " FOREIGN KEY (" + OWNED_IDENTITY + ") REFERENCES " + OwnedIdentity.TABLE_NAME + "(" + OwnedIdentity.OWNED_IDENTITY + ") ON DELETE CASCADE, " + " FOREIGN KEY (" + CONTACT_IDENTITY + ", " + OWNED_IDENTITY + ", " + TRUSTED_DETAILS_VERSION + ") REFERENCES " + ContactIdentityDetails.TABLE_NAME + "(" + ContactIdentityDetails.CONTACT_IDENTITY + ", " + ContactIdentityDetails.OWNED_IDENTITY + ", " + ContactIdentityDetails.VERSION + "), " + @@ -835,11 +861,18 @@ public static void upgradeTable(Session session, int oldVersion, int newVersion) } oldVersion = 28; } + if (oldVersion < 35 && newVersion >= 35) { + try (Statement statement = session.createStatement()) { + Logger.d("MIGRATING `contact_identity` TABLE FROM VERSION " + oldVersion + " TO 35"); + statement.execute("ALTER TABLE contact_identity ADD COLUMN last_no_device_contact_device_discovery INTEGER NOT NULL DEFAULT 0"); + } + oldVersion = 35; + } } @Override public void insert() throws SQLException { - try (PreparedStatement statement = identityManagerSession.session.prepareStatement("INSERT INTO " + TABLE_NAME + " VALUES (?,?,?,?,?, ?,?,?,?);")) { + try (PreparedStatement statement = identityManagerSession.session.prepareStatement("INSERT INTO " + TABLE_NAME + " VALUES (?,?,?,?,?, ?,?,?,?,?);")) { statement.setBytes(1, contactIdentity.getBytes()); statement.setBytes(2, ownedIdentity.getBytes()); statement.setInt(3, trustedDetailsVersion); @@ -850,6 +883,7 @@ public void insert() throws SQLException { statement.setBoolean(7, revokedAsCompromised); statement.setBoolean(8, forcefullyTrustedByUser); statement.setBoolean(9, oneToOne); + statement.setLong(10, lastNoDeviceContactDeviceDiscovery); statement.executeUpdate(); commitHookBits |= HOOK_BIT_INSERTED; identityManagerSession.session.addSessionCommitListener(this); diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/KeycloakServer.java b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/KeycloakServer.java index a4ebeda2..ac5d2142 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/KeycloakServer.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/KeycloakServer.java @@ -73,6 +73,8 @@ public class KeycloakServer implements ObvDatabase { static final String LATEST_REVOCATION_LIST_TIMESTAMP = "latest_revocation_list_timestamp"; private long latestGroupUpdateTimestamp; // the last time groups wre retrieved from the keycloak server static final String LATEST_GROUP_UPDATE_TIMESTAMP = "latest_group_update_timestamp"; + private String ownApiKey; // the api key given to us by keycloak, non null only for the keycloak server of a managed identity + static final String OWN_API_KEY = "own_api_key"; public String getServerUrl() { return serverUrl; @@ -94,6 +96,10 @@ public JsonWebKeySet getJwks() throws Exception { return new JsonWebKeySet(serializedJwks); } + public String getSerializedJwks() { + return serializedJwks; + } + public String getClientId() { return clientId; } @@ -109,10 +115,18 @@ public JsonWebKey getSignatureKey() throws Exception { return JsonWebKey.Factory.newJwk(serializedSignatureKey); } + public String getSerializedSignatureKey() { + return serializedSignatureKey; + } + public String getSelfRevocationTestNonce() { return selfRevocationTestNonce; } + public String getOwnApiKey() { + return ownApiKey; + } + public List getPushTopics() { if (serializedPushTopics == null) { return new ArrayList<>(0); @@ -172,6 +186,7 @@ public KeycloakServer(IdentityManagerSession identityManagerSession, String serv this.selfRevocationTestNonce = null; this.latestRevocationListTimestamp = 0; this.latestGroupUpdateTimestamp = 0; + this.ownApiKey = null; } private KeycloakServer(IdentityManagerSession identityManagerSession, ResultSet res) throws SQLException { @@ -192,6 +207,7 @@ private KeycloakServer(IdentityManagerSession identityManagerSession, ResultSet this.selfRevocationTestNonce = res.getString(SELF_REVOCATION_TEST_NONCE); this.latestRevocationListTimestamp = res.getLong(LATEST_REVOCATION_LIST_TIMESTAMP); this.latestGroupUpdateTimestamp = res.getLong(LATEST_GROUP_UPDATE_TIMESTAMP); + this.ownApiKey = res.getString(OWN_API_KEY); } // endregion @@ -214,6 +230,7 @@ public static void createTable(Session session) throws SQLException { SELF_REVOCATION_TEST_NONCE + " TEXT, " + LATEST_REVOCATION_LIST_TIMESTAMP + " BIGINT NOT NULL, " + LATEST_GROUP_UPDATE_TIMESTAMP + " BIGINT NOT NULL, " + + OWN_API_KEY + " TEXT, " + " CONSTRAINT PK_" + TABLE_NAME + " PRIMARY KEY(" + SERVER_URL + ", " + OWNED_IDENTITY + "), " + " FOREIGN KEY (" + OWNED_IDENTITY + ") REFERENCES " + OwnedIdentity.TABLE_NAME + " (" + OwnedIdentity.OWNED_IDENTITY + ") ON DELETE CASCADE);"); } @@ -256,11 +273,18 @@ public static void upgradeTable(Session session, int oldVersion, int newVersion) } oldVersion = 34; } + if (oldVersion < 35 && newVersion >= 35) { + Logger.d("MIGRATING `keycloak_server` DATABASE FROM VERSION " + oldVersion + " TO 35"); + try (Statement statement = session.createStatement()) { + statement.execute("ALTER TABLE keycloak_server ADD COLUMN `own_api_key` TEXT DEFAULT NULL;"); + } + oldVersion = 35; + } } @Override public void insert() throws SQLException { - try (PreparedStatement statement = identityManagerSession.session.prepareStatement("INSERT INTO " + TABLE_NAME + " VALUES (?,?,?,?,?, ?,?,?,?,?, ?,?);")) { + try (PreparedStatement statement = identityManagerSession.session.prepareStatement("INSERT INTO " + TABLE_NAME + " VALUES (?,?,?,?,?, ?,?,?,?,?, ?,?,?);")) { statement.setString(1, serverUrl); statement.setBytes(2, ownedIdentity.getBytes()); statement.setString(3, serializedJwks); @@ -275,6 +299,7 @@ public void insert() throws SQLException { statement.setLong(11, latestRevocationListTimestamp); statement.setLong(12, latestGroupUpdateTimestamp); + statement.setString(13, ownApiKey); statement.executeUpdate(); } } @@ -352,6 +377,18 @@ public static void saveJwks(IdentityManagerSession identityManagerSession, Strin } } + public static void saveApiKey(IdentityManagerSession identityManagerSession, String serverUrl, Identity ownedIdentity, String apiKey) throws SQLException { + try (PreparedStatement statement = identityManagerSession.session.prepareStatement("UPDATE " + TABLE_NAME + + " SET " + OWN_API_KEY + " = ? " + + " WHERE " + SERVER_URL + " = ? " + + " AND " + OWNED_IDENTITY + " = ?;")) { + statement.setString(1, apiKey); + statement.setString(2, serverUrl); + statement.setBytes(3, ownedIdentity.getBytes()); + statement.executeUpdate(); + } + } + public static void setKeycloakUserId(IdentityManagerSession identityManagerSession, String serverUrl, Identity ownedIdentity, String userId) throws SQLException { try (PreparedStatement statement = identityManagerSession.session.prepareStatement("UPDATE " + TABLE_NAME + " SET " + KEYCLOAK_USER_ID + " = ? " + @@ -377,6 +414,19 @@ public void setKeycloakUserId(String userId) throws SQLException { } } + public void setOwnApiKey(String apiKey) throws SQLException { + try (PreparedStatement statement = identityManagerSession.session.prepareStatement("UPDATE " + TABLE_NAME + + " SET " + OWN_API_KEY + " = ? " + + " WHERE " + SERVER_URL + " = ? " + + " AND " + OWNED_IDENTITY + " = ?;")) { + statement.setString(1, apiKey); + statement.setString(2, this.serverUrl); + statement.setBytes(3, this.ownedIdentity.getBytes()); + statement.executeUpdate(); + this.ownApiKey = apiKey; + } + } + public void setPushTopics(List pushTopics) throws SQLException { byte[] serializedPushTopics; if (pushTopics == null || pushTopics.size() == 0) { diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/OwnedDevice.java b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/OwnedDevice.java index 8d782b2e..9b197074 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/OwnedDevice.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/OwnedDevice.java @@ -23,11 +23,13 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; +import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; +import io.olvid.engine.Logger; import io.olvid.engine.crypto.PRNGService; import io.olvid.engine.datatypes.Identity; import io.olvid.engine.datatypes.ObvDatabase; @@ -54,6 +56,13 @@ public class OwnedDevice implements ObvDatabase { private byte[] serializedDeviceCapabilities; // for the current device, this corresponds to the capabilities that were pushed to contacts. Actual capabilities are static in ObvCapability! static final String SERIALIZED_DEVICE_CAPABILITIES = "serialized_device_capabilities"; + private String displayName; + static final String DISPLAY_NAME = "display_name"; + private Long expirationTimestamp; + static final String EXPIRATION_TIMESTAMP = "expiration_timestamp"; + private Long lastRegistrationTimestamp; + static final String LAST_REGISTRATION_TIMESTAMP = "last_registration_timestamp"; + public UID getUid() { return uid; } @@ -66,8 +75,16 @@ public boolean isCurrentDevice() { return isCurrentDevice; } - public OwnedIdentity getOwnedIdentityObject() throws SQLException { - return OwnedIdentity.get(identityManagerSession, ownedIdentity); + public String getDisplayName() { + return displayName; + } + + public Long getExpirationTimestamp() { + return expirationTimestamp; + } + + public Long getLastRegistrationTimestamp() { + return lastRegistrationTimestamp; } public List getDeviceCapabilities() { @@ -78,26 +95,27 @@ public String[] getRawDeviceCapabilities() { return ObvCapability.deserializeRawDeviceCapabilities(serializedDeviceCapabilities); } - public static OwnedDevice createOtherDevice(IdentityManagerSession identityManagerSession, UID uid, Identity identity) { + public static OwnedDevice createOtherDevice(IdentityManagerSession identityManagerSession, UID uid, Identity identity, String displayName, Long expirationTimestamp, Long lastRegistrationTimestamp, boolean channelCreationAlreadyInProgress) { if (identity == null) { return null; } try { - OwnedDevice ownedDevice = new OwnedDevice(identityManagerSession, uid, identity, false, null); + OwnedDevice ownedDevice = new OwnedDevice(identityManagerSession, uid, identity, false, null, displayName, expirationTimestamp, lastRegistrationTimestamp); ownedDevice.insert(); + ownedDevice.channelCreationAlreadyInProgress = channelCreationAlreadyInProgress; return ownedDevice; } catch (SQLException e) { return null; } } - public static OwnedDevice createCurrentDevice(IdentityManagerSession identityManagerSession, Identity identity, PRNGService prng) { + public static OwnedDevice createCurrentDevice(IdentityManagerSession identityManagerSession, Identity identity, String displayName, PRNGService prng) { if (identity == null) { return null; } UID uid = new UID(prng); try { - OwnedDevice ownedDevice = new OwnedDevice(identityManagerSession, uid, identity, true, null); + OwnedDevice ownedDevice = new OwnedDevice(identityManagerSession, uid, identity, true, null, displayName, null, null); ownedDevice.insert(); return ownedDevice; } catch (SQLException e) { @@ -105,12 +123,15 @@ public static OwnedDevice createCurrentDevice(IdentityManagerSession identityMan } } - private OwnedDevice(IdentityManagerSession identityManagerSession, UID uid, Identity ownedIdentity, boolean isCurrentDevice, byte[] serializedDeviceCapabilities) { + private OwnedDevice(IdentityManagerSession identityManagerSession, UID uid, Identity ownedIdentity, boolean isCurrentDevice, byte[] serializedDeviceCapabilities, String displayName, Long expirationTimestamp, Long lastRegistrationTimestamp) { this.identityManagerSession = identityManagerSession; this.uid = uid; this.ownedIdentity = ownedIdentity; this.isCurrentDevice = isCurrentDevice; this.serializedDeviceCapabilities = serializedDeviceCapabilities; + this.displayName = displayName; + this.expirationTimestamp = expirationTimestamp; + this.lastRegistrationTimestamp = lastRegistrationTimestamp; } private OwnedDevice(IdentityManagerSession identityManagerSession, ResultSet res) throws SQLException { @@ -123,6 +144,15 @@ private OwnedDevice(IdentityManagerSession identityManagerSession, ResultSet res } this.isCurrentDevice = res.getBoolean(IS_CURRENT_DEVICE); this.serializedDeviceCapabilities = res.getBytes(SERIALIZED_DEVICE_CAPABILITIES); + this.displayName = res.getString(DISPLAY_NAME); + this.expirationTimestamp = res.getLong(EXPIRATION_TIMESTAMP); + if (res.wasNull()) { + this.expirationTimestamp = null; + } + this.lastRegistrationTimestamp = res.getLong(LAST_REGISTRATION_TIMESTAMP); + if (res.wasNull()) { + this.lastRegistrationTimestamp = null; + } } @@ -135,6 +165,9 @@ public static void createTable(Session session) throws SQLException { OWNED_IDENTITY + " BLOB NOT NULL, " + IS_CURRENT_DEVICE + " BIT NOT NULL, " + SERIALIZED_DEVICE_CAPABILITIES + " BLOB DEFAULT NULL, " + + DISPLAY_NAME + " TEXT DEFAULT NULL, " + + EXPIRATION_TIMESTAMP + " INTEGER DEFAULT NULL, " + + LAST_REGISTRATION_TIMESTAMP + " INTEGER DEFAULT NULL, " + "FOREIGN KEY (" + OWNED_IDENTITY + ") REFERENCES " + OwnedIdentity.TABLE_NAME + " (" + OwnedIdentity.OWNED_IDENTITY + ") ON DELETE CASCADE);"); } } @@ -146,16 +179,40 @@ public static void upgradeTable(Session session, int oldVersion, int newVersion) } oldVersion = 27; } + if (oldVersion < 35 && newVersion >= 35) { + Logger.d("MIGRATING `owned_device` DATABASE FROM VERSION " + oldVersion + " TO 35"); + try (Statement statement = session.createStatement()) { + statement.execute("ALTER TABLE owned_device ADD COLUMN `display_name` TEXT DEFAULT NULL"); + statement.execute("ALTER TABLE owned_device ADD COLUMN `expiration_timestamp` INTEGER DEFAULT NULL"); + statement.execute("ALTER TABLE owned_device ADD COLUMN `last_registration_timestamp` INTEGER DEFAULT NULL"); + } + oldVersion = 35; + } } @Override public void insert() throws SQLException { - try (PreparedStatement statement = identityManagerSession.session.prepareStatement("INSERT INTO " + TABLE_NAME + " VALUES (?,?,?,?);")) { + try (PreparedStatement statement = identityManagerSession.session.prepareStatement("INSERT INTO " + TABLE_NAME + " VALUES (?,?,?,?,?, ?,?);")) { statement.setBytes(1, uid.getBytes()); statement.setBytes(2, ownedIdentity.getBytes()); statement.setBoolean(3, isCurrentDevice); statement.setBytes(4, serializedDeviceCapabilities); + statement.setString(5, displayName); + if (expirationTimestamp == null) { + statement.setNull(6, Types.INTEGER); + } else { + statement.setLong(6, expirationTimestamp); + } + if (lastRegistrationTimestamp == null) { + statement.setNull(7, Types.INTEGER); + } else { + statement.setLong(7, lastRegistrationTimestamp); + } statement.executeUpdate(); + if (!isCurrentDevice) { + commitHookBits |= HOOK_BIT_INSERTED_OTHER_DEVICE | HOOK_BIT_DEVICES_CHANGED; + identityManagerSession.session.addSessionCommitListener(this); + } } } @@ -164,7 +221,7 @@ public void delete() throws SQLException { try (PreparedStatement statement = identityManagerSession.session.prepareStatement("DELETE FROM " + TABLE_NAME + " WHERE " + UID_ + " = ?;")) { statement.setBytes(1, uid.getBytes()); statement.executeUpdate(); - commitHookBits |= HOOK_BIT_CAPABILITIES_UPDATED; + commitHookBits |= HOOK_BIT_CAPABILITIES_UPDATED | HOOK_BIT_DEVICES_CHANGED; identityManagerSession.session.addSessionCommitListener(this); } } @@ -219,16 +276,30 @@ public static OwnedDevice[] getOtherDevicesOfOwnedIdentity(IdentityManagerSessio } } - public static OwnedDevice[] getAllDevicesOfIdentity(IdentityManagerSession identityManagerSession, Identity identity) throws SQLException { - try (PreparedStatement statement = identityManagerSession.session.prepareStatement("SELECT * FROM " + TABLE_NAME + " WHERE " + - OWNED_IDENTITY + " = ?;")) { + public static List getAllDevicesOfIdentity(IdentityManagerSession identityManagerSession, Identity identity) throws SQLException { + try (PreparedStatement statement = identityManagerSession.session.prepareStatement("SELECT * FROM " + TABLE_NAME + + " WHERE " + OWNED_IDENTITY + " = ?;")) { statement.setBytes(1, identity.getBytes()); try (ResultSet res = statement.executeQuery()) { List list = new ArrayList<>(); while (res.next()) { list.add(new OwnedDevice(identityManagerSession, res)); } - return list.toArray(new OwnedDevice[0]); + return list; + } + } + } + + public static UID[] getAllDeviceUidsOfIdentity(IdentityManagerSession identityManagerSession, Identity identity) throws SQLException { + try (PreparedStatement statement = identityManagerSession.session.prepareStatement("SELECT " + UID_ + " FROM " + TABLE_NAME + " WHERE " + + OWNED_IDENTITY + " = ?;")) { + statement.setBytes(1, identity.getBytes()); + try (ResultSet res = statement.executeQuery()) { + List list = new ArrayList<>(); + while (res.next()) { + list.add(new UID(res.getBytes(UID_))); + } + return list.toArray(new UID[0]); } } } @@ -254,18 +325,74 @@ public void setRawDeviceCapabilities(String[] rawDeviceCapabilities) throws SQLE } } + public void setDisplayName(String displayName) throws SQLException { + try (PreparedStatement statement = identityManagerSession.session.prepareStatement("UPDATE " + TABLE_NAME + + " SET " + DISPLAY_NAME + " = ? " + + " WHERE " + UID_ + " = ? " + + " AND " + OWNED_IDENTITY + " = ?;")) { + statement.setString(1, displayName); + statement.setBytes(2, this.uid.getBytes()); + statement.setBytes(3, this.ownedIdentity.getBytes()); + statement.executeUpdate(); + this.displayName = displayName; + commitHookBits |= HOOK_BIT_DEVICES_CHANGED; + identityManagerSession.session.addSessionCommitListener(this); + } + } + public void setTimestamps(Long expirationTimestamp, Long lastRegistrationTimestamp) throws SQLException { + try (PreparedStatement statement = identityManagerSession.session.prepareStatement("UPDATE " + TABLE_NAME + + " SET " + EXPIRATION_TIMESTAMP + " = ?, " + + LAST_REGISTRATION_TIMESTAMP + " = ? " + + " WHERE " + UID_ + " = ? " + + " AND " + OWNED_IDENTITY + " = ?;")) { + if (expirationTimestamp == null) { + statement.setNull(1, Types.INTEGER); + } else { + statement.setLong(1, expirationTimestamp); + } + if (lastRegistrationTimestamp == null) { + statement.setNull(2, Types.INTEGER); + } else { + statement.setLong(2, lastRegistrationTimestamp); + } + statement.setBytes(3, this.uid.getBytes()); + statement.setBytes(4, this.ownedIdentity.getBytes()); + statement.executeUpdate(); + this.expirationTimestamp = expirationTimestamp; + this.lastRegistrationTimestamp = lastRegistrationTimestamp; + commitHookBits |= HOOK_BIT_DEVICES_CHANGED; + identityManagerSession.session.addSessionCommitListener(this); + } + } + boolean channelCreationAlreadyInProgress = false; private long commitHookBits = 0; + private static final long HOOK_BIT_INSERTED_OTHER_DEVICE = 0x1; private static final long HOOK_BIT_CAPABILITIES_UPDATED = 0x2; + private static final long HOOK_BIT_DEVICES_CHANGED = 0x4; @Override public void wasCommitted() { + // this notification is only caught in the ChannelManager, so as to create a new channel + if ((commitHookBits & HOOK_BIT_INSERTED_OTHER_DEVICE) != 0) { + HashMap userInfo = new HashMap<>(); + userInfo.put(IdentityNotifications.NOTIFICATION_NEW_OWNED_DEVICE_DEVICE_UID_KEY, uid); + userInfo.put(IdentityNotifications.NOTIFICATION_NEW_OWNED_DEVICE_OWNED_IDENTITY_KEY, ownedIdentity); + userInfo.put(IdentityNotifications.NOTIFICATION_NEW_OWNED_DEVICE_CHANNEL_CREATION_ALREADY_IN_PROGRESS_KEY, channelCreationAlreadyInProgress); + identityManagerSession.notificationPostingDelegate.postNotification(IdentityNotifications.NOTIFICATION_NEW_OWNED_DEVICE, userInfo); + } if ((commitHookBits & HOOK_BIT_CAPABILITIES_UPDATED) != 0) { HashMap userInfo = new HashMap<>(); userInfo.put(IdentityNotifications.NOTIFICATION_OWN_CAPABILITIES_UPDATED_OWNED_IDENTITY_KEY, ownedIdentity); identityManagerSession.notificationPostingDelegate.postNotification(IdentityNotifications.NOTIFICATION_OWN_CAPABILITIES_UPDATED, userInfo); } + // this notification is propagated to the App + if ((commitHookBits & HOOK_BIT_DEVICES_CHANGED) != 0) { + HashMap userInfo = new HashMap<>(); + userInfo.put(IdentityNotifications.NOTIFICATION_OWNED_DEVICE_LIST_CHANGED_OWNED_IDENTITY_KEY, ownedIdentity); + identityManagerSession.notificationPostingDelegate.postNotification(IdentityNotifications.NOTIFICATION_OWNED_DEVICE_LIST_CHANGED, userInfo); + } commitHookBits = 0; } } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/OwnedIdentity.java b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/OwnedIdentity.java index ea042c7e..7ac91052 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/OwnedIdentity.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/OwnedIdentity.java @@ -40,9 +40,9 @@ import java.util.List; import java.util.Objects; import java.util.Random; -import java.util.UUID; import io.olvid.engine.Logger; +import io.olvid.engine.crypto.PRNG; import io.olvid.engine.crypto.PRNGService; import io.olvid.engine.crypto.Suite; import io.olvid.engine.datatypes.Constants; @@ -83,12 +83,12 @@ public class OwnedIdentity implements ObvDatabase { static final String PUBLISHED_DETAILS_VERSION = "published_details_version"; private int latestDetailsVersion; static final String LATEST_DETAILS_VERSION = "latest_details_version"; - private UUID apiKey; - static final String API_KEY = "api_key"; private boolean active; static final String ACTIVE = "active"; private String keycloakServerUrl; static final String KEYCLOAK_SERVER_URL = "keycloak_server_url"; + private boolean markedForDeletion; + static final String MARKED_FOR_DELETION = "marked_for_deletion"; public Identity getOwnedIdentity() { return ownedIdentity; @@ -98,10 +98,6 @@ public PrivateIdentity getPrivateIdentity() { return privateIdentity; } - public UUID getApiKey() { - return apiKey; - } - public int getPublishedDetailsVersion() { return publishedDetailsVersion; } @@ -122,6 +118,10 @@ public boolean isKeycloakManaged() { return keycloakServerUrl != null; } + public boolean isMarkedForDeletion() { + return markedForDeletion; + } + // region computed properties public ContactIdentity[] getContactIdentities() { @@ -138,12 +138,7 @@ public UID[] getOtherDeviceUids() throws SQLException { } public UID[] getAllDeviceUids() throws SQLException { - OwnedDevice[] ownedDevices = OwnedDevice.getAllDevicesOfIdentity(identityManagerSession, ownedIdentity); - UID[] uids = new UID[ownedDevices.length]; - for (int i=0; i= newDetailsVersion) { + // if the current version is greater or equal, do nothing + return false; + } + + + // now, create the new OwnedIdentityDetails + OwnedIdentityDetails newPublishedDetails = OwnedIdentityDetails.create(identityManagerSession, ownedIdentity, ownDetailsWithVersionAndPhoto); + if (newPublishedDetails == null) { + Logger.e("In setOwnedIdentityDetailsFromOtherDevice: unable to create new details!"); + throw new SQLException(); + } + + // copy the existing photoUrl if it is the same + if (Objects.equals(newPublishedDetails.getPhotoServerKey(), currentPublishedDetails.getPhotoServerKey()) + && Objects.equals(newPublishedDetails.getPhotoServerLabel(), currentPublishedDetails.getPhotoServerLabel())) { + newPublishedDetails.setPhotoUrl(currentPublishedDetails.getPhotoUrl(), false); + } + + // finally, set the newly created OwnedIdentityDetails as the published ones and cleanup + try (PreparedStatement statement = identityManagerSession.session.prepareStatement("UPDATE " + TABLE_NAME + " SET " + + PUBLISHED_DETAILS_VERSION + " = ?, " + + LATEST_DETAILS_VERSION + " = ? " + + " WHERE " + OWNED_IDENTITY + " = ?;")) { + statement.setInt(1, newDetailsVersion); + statement.setInt(2, newDetailsVersion); + statement.setBytes(3, ownedIdentity.getBytes()); + statement.executeUpdate(); + this.publishedDetailsVersion = newDetailsVersion; + } + + OwnedIdentityDetails.cleanup(identityManagerSession, ownedIdentity, newDetailsVersion, newDetailsVersion); + + hookDetails = newPublishedDetails.getJsonIdentityDetailsWithVersionAndPhoto(); + commitHookBits |= HOOK_BIT_IDENTITY_DETAILS_PUBLISHED | HOOK_BIT_LATEST_IDENTITY_DETAILS_VERSION_CHANGED; + identityManagerSession.session.addSessionCommitListener(this); + + return newPublishedDetails.getPhotoUrl() == null && newPublishedDetails.getPhotoServerKey() != null && newPublishedDetails.getPhotoServerLabel() != null; + } + public void setDetailsDownloadedPhotoUrl(int version, byte[] photo) throws Exception { OwnedIdentityDetails ownedIdentityDetails = OwnedIdentityDetails.get(identityManagerSession, ownedIdentity, version); @@ -401,17 +450,6 @@ public void discardLatestDetails() throws SQLException { } } - public void setApiKey(UUID apiKey) throws SQLException { - try (PreparedStatement statement = identityManagerSession.session.prepareStatement("UPDATE " + TABLE_NAME + - " SET " + API_KEY + " = ? " + - " WHERE " + OWNED_IDENTITY + " = ?;")) { - statement.setString(1, Logger.getUuidString(apiKey)); - statement.setBytes(2, ownedIdentity.getBytes()); - statement.executeUpdate(); - this.apiKey = apiKey; - } - } - public void setActive(boolean active) throws SQLException { try (PreparedStatement statement = identityManagerSession.session.prepareStatement("UPDATE " + TABLE_NAME + " SET " + ACTIVE + " = ? " + @@ -444,12 +482,25 @@ public void setKeycloakServerUrl(String keycloakServerUrl) throws SQLException { } } + public void markForDeletion() throws SQLException { + try (PreparedStatement statement = identityManagerSession.session.prepareStatement("UPDATE " + TABLE_NAME + + " SET " + MARKED_FOR_DELETION + " = ? " + + " WHERE " + OWNED_IDENTITY + " = ?;")) { + statement.setBoolean(1, true); + statement.setBytes(2, ownedIdentity.getBytes()); + statement.executeUpdate(); + this.markedForDeletion = true; + commitHookBits |= HOOK_BIT_IDENTITY_LIST_CHANGED; + identityManagerSession.session.addSessionCommitListener(this); + } + } + // endregion // region constructors - public static OwnedIdentity create(IdentityManagerSession identityManagerSession, String server, Byte serverAuthenticationAlgoImplByte, Byte encryptionAlgoImplByte, JsonIdentityDetails identityDetails, UUID apiKey, PRNGService prng) { - if (identityDetails == null || identityDetails.isEmpty() || apiKey == null) { + public static OwnedIdentity create(IdentityManagerSession identityManagerSession, String server, Byte serverAuthenticationAlgoImplByte, Byte encryptionAlgoImplByte, JsonIdentityDetails identityDetails, String deviceDisplayName, PRNGService prng) { + if (identityDetails == null || identityDetails.isEmpty()) { return null; } KeyPair serverAuthKeyPair = Suite.generateServerAuthenticationKeyPair(serverAuthenticationAlgoImplByte, prng); @@ -462,9 +513,9 @@ public static OwnedIdentity create(IdentityManagerSession identityManagerSession Identity identity = new Identity(server, (ServerAuthenticationPublicKey) serverAuthKeyPair.getPublicKey(), (EncryptionPublicKey) encryptionKeyPair.getPublicKey()); PrivateIdentity privateIdentity = new PrivateIdentity(identity, (ServerAuthenticationPrivateKey) serverAuthKeyPair.getPrivateKey(), (EncryptionPrivateKey) encryptionKeyPair.getPrivateKey(), macKey); OwnedIdentityDetails ownedIdentityDetails = OwnedIdentityDetails.create(identityManagerSession, identity, identityManagerSession.jsonObjectMapper.writeValueAsString(identityDetails)); - OwnedIdentity ownedIdentity = new OwnedIdentity(identityManagerSession, privateIdentity, ownedIdentityDetails.getVersion(), apiKey); + OwnedIdentity ownedIdentity = new OwnedIdentity(identityManagerSession, privateIdentity, ownedIdentityDetails.getVersion()); ownedIdentity.insert(); - OwnedDevice.createCurrentDevice(identityManagerSession, identity, prng); + OwnedDevice.createCurrentDevice(identityManagerSession, identity, deviceDisplayName, prng); return ownedIdentity; } catch (Exception e) { e.printStackTrace(); @@ -472,15 +523,15 @@ public static OwnedIdentity create(IdentityManagerSession identityManagerSession } } - private OwnedIdentity(IdentityManagerSession identityManagerSession, PrivateIdentity privateIdentity, int detailsVersion, UUID apiKey) { + private OwnedIdentity(IdentityManagerSession identityManagerSession, PrivateIdentity privateIdentity, int detailsVersion) { this.identityManagerSession = identityManagerSession; this.ownedIdentity = privateIdentity.getPublicIdentity(); this.privateIdentity = privateIdentity; this.publishedDetailsVersion = detailsVersion; this.latestDetailsVersion = detailsVersion; - this.apiKey = apiKey; this.active = true; this.keycloakServerUrl = null; + this.markedForDeletion = false; } private OwnedIdentity(IdentityManagerSession identityManagerSession, ResultSet res) throws SQLException { @@ -493,13 +544,9 @@ private OwnedIdentity(IdentityManagerSession identityManagerSession, ResultSet r this.privateIdentity = PrivateIdentity.deserialize(res.getBytes(PRIVATE_IDENTITY)); this.publishedDetailsVersion = res.getInt(PUBLISHED_DETAILS_VERSION); this.latestDetailsVersion = res.getInt(LATEST_DETAILS_VERSION); - try { - this.apiKey = UUID.fromString(res.getString(API_KEY)); - } catch (Exception e) { - this.apiKey = null; - } this.active = res.getBoolean(ACTIVE); this.keycloakServerUrl = res.getString(KEYCLOAK_SERVER_URL); + this.markedForDeletion = res.getBoolean(MARKED_FOR_DELETION); } @@ -514,9 +561,9 @@ public static void createTable(Session session) throws SQLException { PRIVATE_IDENTITY + " BLOB NOT NULL, " + PUBLISHED_DETAILS_VERSION + " INT NOT NULL, " + LATEST_DETAILS_VERSION + " INT NOT NULL, " + - API_KEY + " VARCHAR NOT NULL, " + ACTIVE + " BIT NOT NULL, " + KEYCLOAK_SERVER_URL + " TEXT, " + + MARKED_FOR_DELETION + " BIT NOT NULL, " + " FOREIGN KEY (" + OWNED_IDENTITY + ", " + PUBLISHED_DETAILS_VERSION + ") REFERENCES " + OwnedIdentityDetails.TABLE_NAME + "(" + OwnedIdentityDetails.OWNED_IDENTITY + ", " + OwnedIdentityDetails.VERSION + ")," + " FOREIGN KEY (" + OWNED_IDENTITY + ", " + LATEST_DETAILS_VERSION + ") REFERENCES " + OwnedIdentityDetails.TABLE_NAME + "(" + OwnedIdentityDetails.OWNED_IDENTITY + ", " + OwnedIdentityDetails.VERSION + ")," + " FOREIGN KEY (" + OWNED_IDENTITY + ", " + KEYCLOAK_SERVER_URL + ") REFERENCES " + KeycloakServer.TABLE_NAME + "(" + KeycloakServer.OWNED_IDENTITY + ", " + KeycloakServer.SERVER_URL + "));"); @@ -617,6 +664,16 @@ public static void upgradeTable(Session session, int oldVersion, int newVersion) } oldVersion = 20; } + if (oldVersion < 35 && newVersion >= 35) { + PRNG prng = Suite.getDefaultPRNGService(Suite.LATEST_VERSION); + try (Statement statement = session.createStatement()) { + Logger.d("MIGRATING `owned_identity` DATABASE FROM VERSION " + oldVersion + " TO 35"); + // No need to save the api_key, it has already been saved on server side + statement.execute("ALTER TABLE owned_identity DROP COLUMN api_key"); + statement.execute("ALTER TABLE owned_identity ADD COLUMN marked_for_deletion BIT NOT NULL DEFAULT 0"); + } + oldVersion = 35; + } } @Override @@ -626,9 +683,9 @@ public void insert() throws SQLException { statement.setBytes(2, privateIdentity.serialize()); statement.setInt(3, publishedDetailsVersion); statement.setInt(4, latestDetailsVersion); - statement.setString(5, Logger.getUuidString(apiKey)); - statement.setBoolean(6, active); - statement.setString(7, keycloakServerUrl); + statement.setBoolean(5, active); + statement.setString(6, keycloakServerUrl); + statement.setBoolean(7, markedForDeletion); statement.executeUpdate(); commitHookBits |= HOOK_BIT_IDENTITY_LIST_CHANGED; identityManagerSession.session.addSessionCommitListener(this); @@ -690,21 +747,10 @@ public static boolean isActive(IdentityManagerSession identityManagerSession, Id } } + // only returns owned identities that have not been marked for deletion public static OwnedIdentity[] getAll(IdentityManagerSession identityManagerSession) throws SQLException { - try (PreparedStatement statement = identityManagerSession.session.prepareStatement("SELECT * FROM " + TABLE_NAME + ";")) { - try (ResultSet res = statement.executeQuery()) { - List list = new ArrayList<>(); - while (res.next()) { - list.add(new OwnedIdentity(identityManagerSession, res)); - } - return list.toArray(new OwnedIdentity[0]); - } - } - } - - - public static OwnedIdentity[] getAllActive(IdentityManagerSession identityManagerSession) throws SQLException { - try (PreparedStatement statement = identityManagerSession.session.prepareStatement("SELECT * FROM " + TABLE_NAME + " WHERE " + ACTIVE + " = 1;")) { + try (PreparedStatement statement = identityManagerSession.session.prepareStatement("SELECT * FROM " + TABLE_NAME + + " WHERE " + MARKED_FOR_DELETION + " == 0;")) { try (ResultSet res = statement.executeQuery()) { List list = new ArrayList<>(); while (res.next()) { @@ -804,7 +850,6 @@ private Pojo_0 backup() throws SQLException { if (latestDetailsVersion != publishedDetailsVersion) { pojo.latest_details = getLatestDetails().backup(); } - pojo.api_key = Logger.getUuidString(apiKey); pojo.active = active; if (keycloakServerUrl != null) { pojo.keycloak = getKeycloakServer().backup(); @@ -815,7 +860,7 @@ private Pojo_0 backup() throws SQLException { return pojo; } - public static ObvIdentity restore(IdentityManagerSession identityManagerSession, Pojo_0 pojo, PRNGService prng) throws SQLException { + public static ObvIdentity restore(IdentityManagerSession identityManagerSession, Pojo_0 pojo, String deviceDisplayName, PRNGService prng) throws SQLException { Identity ownedIdentity = null; try { ownedIdentity = Identity.of(pojo.owned_identity); @@ -836,13 +881,11 @@ public static ObvIdentity restore(IdentityManagerSession identityManagerSession, if (pojo.latest_details != null && pojo.latest_details.version != pojo.published_details.version) { latest_details = OwnedIdentityDetails.restore(identityManagerSession, ownedIdentity, pojo.latest_details); } - OwnedIdentity ownedIdentityObject = new OwnedIdentity(identityManagerSession, privateIdentity, published_details.getVersion(), UUID.fromString(pojo.api_key)); + OwnedIdentity ownedIdentityObject = new OwnedIdentity(identityManagerSession, privateIdentity, published_details.getVersion()); if (latest_details != null) { ownedIdentityObject.latestDetailsVersion = latest_details.getVersion(); } - // after a restore, the identity is marked inactive in the Engine (to delay all message sending/reception until the device is registered on server). - // It will be re-activated once a register push notification is successful - ownedIdentityObject.active = false; + ownedIdentityObject.active = pojo.active; ownedIdentityObject.insert(); if (pojo.keycloak != null) { @@ -852,7 +895,7 @@ public static ObvIdentity restore(IdentityManagerSession identityManagerSession, } } - OwnedDevice currentOwnedDevice = OwnedDevice.createCurrentDevice(identityManagerSession, ownedIdentity, prng); + OwnedDevice currentOwnedDevice = OwnedDevice.createCurrentDevice(identityManagerSession, ownedIdentity, deviceDisplayName, prng); // when restoring a backup, directly set all currentDevices to the most up to data capabilities // rationale: all channels will be recreated and contact devices will be notified properly. currentOwnedDevice.setRawDeviceCapabilities(ObvCapability.capabilityListToStringArray(ObvCapability.currentCapabilities)); @@ -882,13 +925,13 @@ private static PrivateIdentity restorePrivateIdentity(Identity publicIdentity, P } } + @JsonIgnoreProperties(ignoreUnknown = true) public static class Pojo_0 { public byte[] owned_identity; public PrivateIdentityPojo_0 private_identity; public OwnedIdentityDetails.Pojo_0 published_details; public OwnedIdentityDetails.Pojo_0 latest_details; - public String api_key; public Boolean active; public KeycloakServer.Pojo_0 keycloak; public ContactIdentity.Pojo_0[] contact_identities; diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/OwnedIdentityDetails.java b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/OwnedIdentityDetails.java index 0c7e29a9..6ee32c73 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/OwnedIdentityDetails.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/OwnedIdentityDetails.java @@ -134,6 +134,22 @@ public static OwnedIdentityDetails create(IdentityManagerSession identityManager } } + public static OwnedIdentityDetails create(IdentityManagerSession identityManagerSession, Identity ownedIdentity, JsonIdentityDetailsWithVersionAndPhoto jsonIdentityDetailsWithVersionAndPhoto) { + if (ownedIdentity == null || jsonIdentityDetailsWithVersionAndPhoto == null) { + return null; + } + try { + OwnedIdentityDetails ownedIdentityDetails = new OwnedIdentityDetails(identityManagerSession, ownedIdentity, jsonIdentityDetailsWithVersionAndPhoto.getVersion(), identityManagerSession.jsonObjectMapper.writeValueAsString(jsonIdentityDetailsWithVersionAndPhoto.getIdentityDetails())); + ownedIdentityDetails.photoServerKey = jsonIdentityDetailsWithVersionAndPhoto.getPhotoServerKey() == null ? null : (AuthEncKey) new Encoded(jsonIdentityDetailsWithVersionAndPhoto.getPhotoServerKey()).decodeSymmetricKey(); + ownedIdentityDetails.photoServerLabel = jsonIdentityDetailsWithVersionAndPhoto.getPhotoServerLabel() == null ? null : new UID(jsonIdentityDetailsWithVersionAndPhoto.getPhotoServerLabel()); + ownedIdentityDetails.insert(); + return ownedIdentityDetails; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + public static OwnedIdentityDetails copy(IdentityManagerSession identityManagerSession, Identity ownedIdentity, int version) { if (ownedIdentity == null) { return null; @@ -293,7 +309,7 @@ public static List getAllPhotoUrl(IdentityManagerSession identityManager } } - public static List getAllWithMissinPhotoUrl(IdentityManagerSession identityManagerSession) throws SQLException { + public static List getAllWithMissingPhotoUrl(IdentityManagerSession identityManagerSession) throws SQLException { try (PreparedStatement statement = identityManagerSession.session.prepareStatement("SELECT * FROM " + TABLE_NAME + " WHERE " + PHOTO_URL + " IS NULL " + " AND " + PHOTO_SERVER_KEY + " IS NOT NULL " + diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/ContactSyncSnapshot.java b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/ContactSyncSnapshot.java new file mode 100644 index 00000000..2883894d --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/ContactSyncSnapshot.java @@ -0,0 +1,48 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.identity.databases.sync; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.sql.SQLException; +import java.util.List; + +import io.olvid.engine.engine.types.sync.ObvSyncDiff; +import io.olvid.engine.engine.types.sync.ObvSyncSnapshotNode; +import io.olvid.engine.identity.databases.ContactIdentity; +import io.olvid.engine.identity.datatypes.IdentityManagerSession; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ContactSyncSnapshot implements ObvSyncSnapshotNode { + IdentityDetailsSyncSnapshot trustedDetails; + public static ContactSyncSnapshot of(IdentityManagerSession identityManagerSession, ContactIdentity contact) throws SQLException { + return null; + } + + @Override + public boolean areContentsTheSame(ObvSyncSnapshotNode otherSnapshotNode) { + return false; + } + + @Override + public List computeDiff(ObvSyncSnapshotNode otherSnapshotNode) throws Exception { + return null; + } +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/GroupV1SyncSnapshot.java b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/GroupV1SyncSnapshot.java new file mode 100644 index 00000000..a26d5d44 --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/GroupV1SyncSnapshot.java @@ -0,0 +1,50 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.identity.databases.sync; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.sql.SQLException; +import java.util.List; + +import io.olvid.engine.engine.types.sync.ObvSyncDiff; +import io.olvid.engine.engine.types.sync.ObvSyncSnapshotNode; +import io.olvid.engine.identity.databases.ContactGroup; +import io.olvid.engine.identity.datatypes.IdentityManagerSession; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class GroupV1SyncSnapshot implements ObvSyncSnapshotNode { + public static GroupV1SyncSnapshot of(IdentityManagerSession identityManagerSession, ContactGroup group) throws SQLException { + // TODO + return null; + } + + @Override + public boolean areContentsTheSame(ObvSyncSnapshotNode otherSnapshotNode) { + // TODO + return false; + } + + @Override + public List computeDiff(ObvSyncSnapshotNode otherSnapshotNode) throws Exception { + // TODO + return null; + } +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/GroupV2SyncSnapshot.java b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/GroupV2SyncSnapshot.java new file mode 100644 index 00000000..83b8f839 --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/GroupV2SyncSnapshot.java @@ -0,0 +1,50 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.identity.databases.sync; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.sql.SQLException; +import java.util.List; + +import io.olvid.engine.engine.types.sync.ObvSyncDiff; +import io.olvid.engine.engine.types.sync.ObvSyncSnapshotNode; +import io.olvid.engine.identity.databases.ContactGroupV2; +import io.olvid.engine.identity.datatypes.IdentityManagerSession; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class GroupV2SyncSnapshot implements ObvSyncSnapshotNode { + public static GroupV2SyncSnapshot of(IdentityManagerSession identityManagerSession, ContactGroupV2 group2) throws SQLException { + // TODO + return null; + } + + @Override + public boolean areContentsTheSame(ObvSyncSnapshotNode otherSnapshotNode) { + // TODO + return false; + } + + @Override + public List computeDiff(ObvSyncSnapshotNode otherSnapshotNode) throws Exception { + // TODO + return null; + } +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/IdentityDetailsSyncSnapshot.java b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/IdentityDetailsSyncSnapshot.java new file mode 100644 index 00000000..a87f26ef --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/IdentityDetailsSyncSnapshot.java @@ -0,0 +1,116 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.identity.databases.sync; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; + +import io.olvid.engine.encoder.DecodingException; +import io.olvid.engine.encoder.Encoded; +import io.olvid.engine.engine.types.sync.ObvSyncDiff; +import io.olvid.engine.engine.types.sync.ObvSyncSnapshotNode; +import io.olvid.engine.identity.databases.OwnedIdentityDetails; +import io.olvid.engine.identity.datatypes.IdentityManagerSession; + +// This class is used for both owned identity and contacts +@JsonIgnoreProperties(ignoreUnknown = true) +public class IdentityDetailsSyncSnapshot implements ObvSyncSnapshotNode { + public static final String VERSION = "version"; + public static final String SERIALIZED_DETAILS = "serialized_details"; + public static final String PHOTO_SERVER_LABEL = "photo_server_label"; + public static final String PHOTO_SERVER_KEY = "photo_server_key"; + static HashSet DEFAULT_DOMAIN = new HashSet<>(Arrays.asList(VERSION, SERIALIZED_DETAILS, PHOTO_SERVER_LABEL, PHOTO_SERVER_KEY)); + + + public Integer version; + public String serialized_details; + public byte[] photo_server_label; + public byte[] photo_server_key; + public HashSet domain; + + + public static IdentityDetailsSyncSnapshot of(IdentityManagerSession identityManagerSession, OwnedIdentityDetails publishedDetails) { + IdentityDetailsSyncSnapshot identityDetailsSyncSnapshot = new IdentityDetailsSyncSnapshot(); + identityDetailsSyncSnapshot.version = publishedDetails.getVersion(); + identityDetailsSyncSnapshot.serialized_details = publishedDetails.getSerializedJsonDetails(); + identityDetailsSyncSnapshot.photo_server_label = publishedDetails.getPhotoServerLabel().getBytes(); + identityDetailsSyncSnapshot.photo_server_key = Encoded.of(publishedDetails.getPhotoServerKey()).getBytes(); + identityDetailsSyncSnapshot.domain = DEFAULT_DOMAIN; + return identityDetailsSyncSnapshot; + } + + @Override + public boolean areContentsTheSame(ObvSyncSnapshotNode otherSnapshotNode) { + if (!(otherSnapshotNode instanceof IdentityDetailsSyncSnapshot)) { + return false; + } + + IdentityDetailsSyncSnapshot other = (IdentityDetailsSyncSnapshot) otherSnapshotNode; + HashSet domainIntersection = new HashSet<>(domain); + domainIntersection.retainAll(other.domain); + + for (String item : domainIntersection) { + switch (item) { + case VERSION: { + if (!Objects.equals(version, other.version)) { + return false; + } + break; + } + case SERIALIZED_DETAILS: { + // TODO: we need to deserialize here for the comparison + if (!Objects.equals(serialized_details, other.serialized_details)) { + return false; + } + break; + } + case PHOTO_SERVER_LABEL: { + if (!Arrays.equals(photo_server_label, other.photo_server_label)) { + return false; + } + break; + } + case PHOTO_SERVER_KEY: { + try { + if ((photo_server_key == null && other.photo_server_key != null) + || (photo_server_key != null && other.photo_server_key == null) + || (photo_server_key != null && !Objects.equals(new Encoded(photo_server_key).decodeSymmetricKey(), new Encoded(other.photo_server_key).decodeSymmetricKey()))) { + return false; + } + } catch (DecodingException e) { + return false; + } + break; + } + } + } + return true; + } + + @Override + public List computeDiff(ObvSyncSnapshotNode otherSnapshotNode) throws Exception { + // TODO + return null; + } +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/IdentityManagerSyncSnapshot.java b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/IdentityManagerSyncSnapshot.java new file mode 100644 index 00000000..a08667e2 --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/IdentityManagerSyncSnapshot.java @@ -0,0 +1,112 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.identity.databases.sync; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.engine.types.sync.ObvSyncDiff; +import io.olvid.engine.engine.types.sync.ObvSyncSnapshotNode; +import io.olvid.engine.identity.databases.OwnedIdentity; +import io.olvid.engine.identity.datatypes.IdentityManagerSession; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class IdentityManagerSyncSnapshot implements ObvSyncSnapshotNode { + public static final String OWNED_IDENTITY = "owned_identity"; + public static final String OWNED_IDENTITY_NODE = "owned_identity_node"; + static HashSet DEFAULT_DOMAIN = new HashSet<>(Arrays.asList(OWNED_IDENTITY, OWNED_IDENTITY_NODE)); + + public byte[] owned_identity; + public OwnedIdentitySyncSnapshot owned_identity_node; + public HashSet domain; + + public static IdentityManagerSyncSnapshot of(IdentityManagerSession identityManagerSession, Identity ownedIdentity) throws SQLException { + IdentityManagerSyncSnapshot identityManagerSyncSnapshot = new IdentityManagerSyncSnapshot(); + identityManagerSyncSnapshot.owned_identity = ownedIdentity.getBytes(); + OwnedIdentity ownedIdentityObject = OwnedIdentity.get(identityManagerSession, ownedIdentity); + if (ownedIdentityObject != null) { + identityManagerSyncSnapshot.owned_identity_node = OwnedIdentitySyncSnapshot.of(identityManagerSession, ownedIdentityObject); + } + identityManagerSyncSnapshot.domain = DEFAULT_DOMAIN; + return identityManagerSyncSnapshot; + } + + @Override + public boolean areContentsTheSame(ObvSyncSnapshotNode otherSnapshotNode) { + if (!(otherSnapshotNode instanceof IdentityManagerSyncSnapshot)) { + return false; + } + + IdentityManagerSyncSnapshot other = (IdentityManagerSyncSnapshot) otherSnapshotNode; + HashSet domainIntersection = new HashSet<>(domain); + domainIntersection.retainAll(other.domain); + + for (String item : domainIntersection) { + switch (item) { + case OWNED_IDENTITY: { + if (!Arrays.equals(owned_identity, other.owned_identity)) { + return false; + } + break; + } + case OWNED_IDENTITY_NODE: { + if (!owned_identity_node.areContentsTheSame(other.owned_identity_node)) { + return false; + } + break; + } + } + } + return true; + } + + @Override + public List computeDiff(ObvSyncSnapshotNode otherSnapshotNode) throws Exception { + if (!(otherSnapshotNode instanceof IdentityManagerSyncSnapshot)) { + throw new Exception(); + } + IdentityManagerSyncSnapshot other = (IdentityManagerSyncSnapshot) otherSnapshotNode; + HashSet domainIntersection = new HashSet<>(domain); + domainIntersection.retainAll(other.domain); + + List diffs = new ArrayList<>(); + for (String item : domainIntersection) { + switch (item) { + case OWNED_IDENTITY: { + if (!Arrays.equals(owned_identity, other.owned_identity)) { + throw new Exception(); + } + break; + } + case OWNED_IDENTITY_NODE: { + diffs.addAll(owned_identity_node.computeDiff(other.owned_identity_node)); + break; + } + } + } + return diffs; + } +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/KeycloakSyncSnapshot.java b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/KeycloakSyncSnapshot.java new file mode 100644 index 00000000..1c843c58 --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/KeycloakSyncSnapshot.java @@ -0,0 +1,116 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.identity.databases.sync; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.sql.SQLException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; + +import io.olvid.engine.engine.types.sync.ObvSyncDiff; +import io.olvid.engine.engine.types.sync.ObvSyncSnapshotNode; +import io.olvid.engine.identity.databases.KeycloakServer; +import io.olvid.engine.identity.datatypes.IdentityManagerSession; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class KeycloakSyncSnapshot implements ObvSyncSnapshotNode { + public static final String SERVER_URL = "server_url"; + public static final String CLIENT_ID = "client_id"; + public static final String CLIENT_SECRET = "client_secret"; + public static final String KEYCLOAK_USER_ID = "keycloak_user_id"; + public static final String SELF_REVOCATION_TEST_NONCE = "self_revocation_test_nonce"; + static HashSet DEFAULT_DOMAIN = new HashSet<>(Arrays.asList(SERVER_URL, CLIENT_ID, CLIENT_SECRET, KEYCLOAK_USER_ID, SELF_REVOCATION_TEST_NONCE)); + + + public String server_url; + public String client_id; + public String client_secret; + public String keycloak_user_id; + public String self_revocation_test_nonce; + public HashSet domain; + + + public static KeycloakSyncSnapshot of(IdentityManagerSession identityManagerSession, KeycloakServer keycloakServer) throws SQLException { + KeycloakSyncSnapshot keycloakSyncSnapshot = new KeycloakSyncSnapshot(); + keycloakSyncSnapshot.server_url = keycloakServer.getServerUrl(); + keycloakSyncSnapshot.client_id = keycloakServer.getClientId(); + keycloakSyncSnapshot.client_secret = keycloakServer.getClientSecret(); + keycloakSyncSnapshot.keycloak_user_id = keycloakServer.getKeycloakUserId(); + keycloakSyncSnapshot.self_revocation_test_nonce = keycloakServer.getSelfRevocationTestNonce(); + keycloakSyncSnapshot.domain = DEFAULT_DOMAIN; + return keycloakSyncSnapshot; + } + + @Override + public boolean areContentsTheSame(ObvSyncSnapshotNode otherSnapshotNode) { + if (!(otherSnapshotNode instanceof KeycloakSyncSnapshot)) { + return false; + } + + KeycloakSyncSnapshot other = (KeycloakSyncSnapshot) otherSnapshotNode; + HashSet domainIntersection = new HashSet<>(domain); + domainIntersection.retainAll(other.domain); + + for (String item : domainIntersection) { + switch (item) { + case SERVER_URL: { + if (!Objects.equals(server_url, other.server_url)) { + return false; + } + break; + } + case CLIENT_ID: { + if (!Objects.equals(client_id, other.client_id)) { + return false; + } + break; + } + case CLIENT_SECRET: { + if (!Objects.equals(client_secret, other.client_secret)) { + return false; + } + break; + } + case KEYCLOAK_USER_ID: { + if (!Objects.equals(keycloak_user_id, other.keycloak_user_id)) { + return false; + } + break; + } + case SELF_REVOCATION_TEST_NONCE: { + if (!Objects.equals(self_revocation_test_nonce, other.self_revocation_test_nonce)) { + return false; + } + break; + } + } + } + return true; + } + + @Override + public List computeDiff(ObvSyncSnapshotNode otherSnapshotNode) throws Exception { + // TODO + return null; + } +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/OwnedIdentitySyncSnapshot.java b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/OwnedIdentitySyncSnapshot.java new file mode 100644 index 00000000..775dd5fb --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/identity/databases/sync/OwnedIdentitySyncSnapshot.java @@ -0,0 +1,115 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.identity.databases.sync; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.sql.SQLException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +import io.olvid.engine.engine.types.ObvBytesKey; +import io.olvid.engine.engine.types.ObvGroupOwnerAndUidKey; +import io.olvid.engine.engine.types.sync.ObvSyncDiff; +import io.olvid.engine.engine.types.sync.ObvSyncSnapshotNode; +import io.olvid.engine.identity.databases.ContactGroup; +import io.olvid.engine.identity.databases.ContactGroupV2; +import io.olvid.engine.identity.databases.ContactIdentity; +import io.olvid.engine.identity.databases.KeycloakServer; +import io.olvid.engine.identity.databases.OwnedIdentity; +import io.olvid.engine.identity.databases.OwnedIdentityDetails; +import io.olvid.engine.identity.datatypes.IdentityManagerSession; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class OwnedIdentitySyncSnapshot implements ObvSyncSnapshotNode { + public static final String PUBLISHED_DETAILS = "published_details"; + public static final String KEYCLOAK = "keycloak"; + public static final String CONTACTS = "contacts"; + public static final String GROUPS = "groups"; + public static final String GROUPS2 = "groups2"; + static HashSet DEFAULT_DOMAIN = new HashSet<>(Arrays.asList(PUBLISHED_DETAILS, KEYCLOAK, CONTACTS, GROUPS, GROUPS2)); + + + public IdentityDetailsSyncSnapshot published_details; + public KeycloakSyncSnapshot keycloak; + @JsonSerialize(keyUsing = ObvBytesKey.Serializer.class) + @JsonDeserialize(keyUsing = ObvBytesKey.KeyDeserializer.class) + public HashMap contacts; + + @JsonSerialize(keyUsing = ObvGroupOwnerAndUidKey.Serializer.class) + @JsonDeserialize(keyUsing = ObvGroupOwnerAndUidKey.Deserializer.class) + public HashMap groups; + + @JsonSerialize(keyUsing = ObvBytesKey.Serializer.class) + @JsonDeserialize(keyUsing = ObvBytesKey.KeyDeserializer.class) + public HashMap groups2; + public HashSet domain; + + + public static OwnedIdentitySyncSnapshot of(IdentityManagerSession identityManagerSession, OwnedIdentity ownedIdentity) throws SQLException { + OwnedIdentitySyncSnapshot ownedIdentitySyncSnapshot = new OwnedIdentitySyncSnapshot(); + + OwnedIdentityDetails publishedDetails = OwnedIdentityDetails.get(identityManagerSession, ownedIdentity.getOwnedIdentity(), ownedIdentity.getPublishedDetailsVersion()); + if (publishedDetails != null) { + ownedIdentitySyncSnapshot.published_details = IdentityDetailsSyncSnapshot.of(identityManagerSession, publishedDetails); + } + + if (ownedIdentity.isKeycloakManaged()) { + KeycloakServer keycloakServer = KeycloakServer.get(identityManagerSession, ownedIdentity.getKeycloakServerUrl(), ownedIdentity.getOwnedIdentity()); + if (keycloakServer != null) { + ownedIdentitySyncSnapshot.keycloak = KeycloakSyncSnapshot.of(identityManagerSession, keycloakServer); + } + } + + ownedIdentitySyncSnapshot.contacts = new HashMap<>(); + for (ContactIdentity contact : ContactIdentity.getAll(identityManagerSession, ownedIdentity.getOwnedIdentity())) { + ownedIdentitySyncSnapshot.contacts.put(new ObvBytesKey(contact.getContactIdentity().getBytes()), ContactSyncSnapshot.of(identityManagerSession, contact)); + } + + ownedIdentitySyncSnapshot.groups = new HashMap<>(); + for (ContactGroup group : ContactGroup.getAllForIdentity(identityManagerSession, ownedIdentity.getOwnedIdentity())) { + ownedIdentitySyncSnapshot.groups.put(new ObvGroupOwnerAndUidKey(group.getGroupOwnerAndUid()), GroupV1SyncSnapshot.of(identityManagerSession, group)); + } + + ownedIdentitySyncSnapshot.groups2 = new HashMap<>(); + for (ContactGroupV2 group2 : ContactGroupV2.getAllForIdentity(identityManagerSession, ownedIdentity.getOwnedIdentity())) { + ownedIdentitySyncSnapshot.groups2.put(new ObvBytesKey(group2.getGroupIdentifier().getBytes()), GroupV2SyncSnapshot.of(identityManagerSession, group2)); + } + + ownedIdentitySyncSnapshot.domain = DEFAULT_DOMAIN; + return ownedIdentitySyncSnapshot; + } + + @Override + public boolean areContentsTheSame(ObvSyncSnapshotNode otherSnapshotNode) { + // TODO + return false; + } + + @Override + public List computeDiff(ObvSyncSnapshotNode otherSnapshotNode) throws Exception { + // TODO + return null; + } +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/BackupDelegate.java b/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/BackupDelegate.java index 43e91a64..4fc4dd6d 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/BackupDelegate.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/BackupDelegate.java @@ -19,7 +19,6 @@ package io.olvid.engine.metamanager; -import io.olvid.engine.datatypes.Identity; import io.olvid.engine.datatypes.UID; import io.olvid.engine.engine.types.ObvBackupKeyInformation; import io.olvid.engine.engine.types.identities.ObvIdentity; @@ -37,6 +36,6 @@ public interface BackupDelegate { void markBackupUploaded(UID backupKeyUid, int version); void discardBackup(UID backupKeyUid, int version); int validateBackupSeed(String seedString, byte[] backupContent); - ObvIdentity[] restoreOwnedIdentitiesFromBackup(String seedString, byte[] backupContent); - void restoreContactsAndGroupsFromBackup(String seedString, byte[] backupContent, Identity[] restoredOwnedIdentities); + ObvIdentity[] restoreOwnedIdentitiesFromBackup(String seedString, byte[] backupContent, String deviceDisplayName); + void restoreContactsAndGroupsFromBackup(String seedString, byte[] backupContent, ObvIdentity[] restoredOwnedIdentities); } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/ChannelDelegate.java b/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/ChannelDelegate.java index d5b05248..b8d2b776 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/ChannelDelegate.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/ChannelDelegate.java @@ -45,5 +45,6 @@ public interface ChannelDelegate { void deleteObliviousChannelsWithContact(Session session, Identity ownedIdentity, Identity remoteIdentity) throws Exception; void deleteObliviousChannelIfItExists(Session session, Identity ownedIdentity, UID remoteDeviceUid, Identity remoteIdentity) throws Exception; void deleteAllChannelsForOwnedIdentity(Session session, Identity ownedIdentity) throws SQLException; - boolean checkIfObliviousChannelExists(Session session, Identity ownedIdentity, UID remoteDeviceUid, Identity remoteIdentity) throws Exception; + boolean checkIfObliviousChannelExists(Session session, Identity ownedIdentity, UID remoteDeviceUid, Identity remoteIdentity) throws SQLException; + boolean checkIfObliviousChannelIsConfirmed(Session session, Identity ownedIdentity, UID remoteDeviceUid, Identity remoteIdentity) throws SQLException; } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/EncryptionForIdentityDelegate.java b/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/EncryptionForIdentityDelegate.java index e5f1e18f..4e63fbc7 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/EncryptionForIdentityDelegate.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/EncryptionForIdentityDelegate.java @@ -31,4 +31,5 @@ public interface EncryptionForIdentityDelegate { EncryptedBytes wrap(AuthEncKey messageKey, Identity toIdentity, PRNGService prng); AuthEncKey unwrap(Session session, EncryptedBytes wrappedKey, Identity toIdentity) throws SQLException; + byte[] decrypt(Session session, EncryptedBytes ciphertext, Identity toIdentity) throws SQLException; } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/EngineOwnedIdentityCleanupDelegate.java b/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/EngineOwnedIdentityCleanupDelegate.java new file mode 100644 index 00000000..aa850054 --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/EngineOwnedIdentityCleanupDelegate.java @@ -0,0 +1,29 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.metamanager; + +import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.datatypes.Session; +import io.olvid.engine.datatypes.UID; + +public interface EngineOwnedIdentityCleanupDelegate { + void deleteOwnedIdentityFromInboxOutboxProtocolsAndDialogs(Session session, Identity ownedIdentity, UID excludedProtocolInstanceUid) throws Exception; + void deleteOwnedIdentityServerSession(Session session, Identity ownedIdentity) throws Exception; +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/IdentityDelegate.java b/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/IdentityDelegate.java index 65905ddc..92c4df6c 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/IdentityDelegate.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/IdentityDelegate.java @@ -29,7 +29,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.UUID; import io.olvid.engine.crypto.PRNGService; import io.olvid.engine.datatypes.Constants; @@ -54,24 +53,28 @@ import io.olvid.engine.engine.types.JsonIdentityDetailsWithVersionAndPhoto; import io.olvid.engine.engine.types.JsonKeycloakUserDetails; import io.olvid.engine.engine.types.ObvCapability; +import io.olvid.engine.engine.types.sync.ObvSyncAtom; import io.olvid.engine.engine.types.identities.ObvContactActiveOrInactiveReason; import io.olvid.engine.engine.types.identities.ObvGroupV2; import io.olvid.engine.engine.types.identities.ObvIdentity; import io.olvid.engine.engine.types.identities.ObvKeycloakState; +import io.olvid.engine.engine.types.identities.ObvOwnedDevice; import io.olvid.engine.identity.datatypes.KeycloakGroupBlob; public interface IdentityDelegate { boolean isOwnedIdentity(Session session, Identity ownedIdentity) throws SQLException; boolean isActiveOwnedIdentity(Session session, Identity ownedIdentity) throws SQLException; - Identity generateOwnedIdentity(Session session, String server, JsonIdentityDetails jsonIdentityDetails, UUID apiKey, ObvKeycloakState keycloakState, PRNGService prng) throws SQLException; + Identity generateOwnedIdentity(Session session, String server, JsonIdentityDetails jsonIdentityDetails, ObvKeycloakState keycloakState, String deviceDisplayName, PRNGService prng) throws SQLException; void deleteOwnedIdentity(Session session, Identity ownedIdentity) throws SQLException; Identity[] getOwnedIdentities(Session session) throws SQLException; void updateLatestIdentityDetails(Session session, Identity ownedIdentity, JsonIdentityDetails jsonIdentityDetails) throws Exception; void updateOwnedIdentityPhoto(Session session, Identity ownedIdentity, String absolutePhotoUrl) throws Exception; void setOwnedDetailsDownloadedPhoto(Session session, Identity ownedIdentity, int version, byte[] decryptedPhoto) throws Exception; void setOwnedIdentityDetailsServerLabelAndKey(Session session, Identity ownedIdentity, int version, UID photoServerLabel, AuthEncKey photoServerKey) throws Exception; + void createOwnedIdentityServerUserData(Session session, Identity ownedIdentity, UID photoServerLabel) throws SQLException; int publishLatestIdentityDetails(Session session, Identity ownedIdentity) throws SQLException; void discardLatestIdentityDetails(Session session, Identity ownedIdentity) throws SQLException; + boolean setOwnedIdentityDetailsFromOtherDevice(Session session, Identity ownedIdentity, JsonIdentityDetailsWithVersionAndPhoto ownDetailsWithVersionAndPhoto) throws SQLException; JsonIdentityDetailsWithVersionAndPhoto[] getOwnedIdentityPublishedAndLatestDetails(Session session, Identity ownedIdentity) throws SQLException; String getSerializedPublishedDetailsOfOwnedIdentity(Session session, Identity ownedIdentity); JsonIdentityDetailsWithVersionAndPhoto getOwnedIdentityPublishedDetails(Session session, Identity ownedIdentity) throws SQLException; @@ -89,31 +92,35 @@ public interface IdentityDelegate { String getOwnedIdentityKeycloakServerUrl(Session session, Identity ownedIdentity) throws SQLException; void saveKeycloakAuthState(Session session, Identity ownedIdentity, String serializedAuthState) throws SQLException; void saveKeycloakJwks(Session session, Identity ownedIdentity, String serializedJwks) throws SQLException; + void saveKeycloakApiKey(Session session, Identity ownedIdentity, String apiKey) throws SQLException; String getOwnedIdentityKeycloakUserId(Session session, Identity ownedIdentity) throws SQLException; void setOwnedIdentityKeycloakUserId(Session session, Identity ownedIdentity, String userId) throws SQLException; void bindOwnedIdentityToKeycloak(Session session, Identity ownedIdentity, String keycloakUserId, ObvKeycloakState keycloakState) throws Exception; int unbindOwnedIdentityFromKeycloak(Session session, Identity ownedIdentity) throws Exception; // return the version of the new details to publish - void updateApiKeyOfOwnedIdentity(Session session, Identity ownedIdentity, UUID newApiKey) throws SQLException; boolean updateKeycloakPushTopicsIfNeeded(Session session, Identity ownedIdentity, String serverUrl, List pushTopics) throws SQLException; void setOwnedIdentityKeycloakSelfRevocationTestNonce(Session session, Identity ownedIdentity, String serverUrl, String nonce) throws SQLException; String getOwnedIdentityKeycloakSelfRevocationTestNonce(Session session, Identity ownedIdentity, String serverUrl) throws SQLException; void updateKeycloakGroups(Session session, Identity ownedIdentity, List signedGroupBlobs, List signedGroupDeletions, List signedGroupKicks, long keycloakCurrentTimestamp) throws Exception; void reactivateOwnedIdentityIfNeeded(Session session, Identity ownedIdentity) throws SQLException; void deactivateOwnedIdentity(Session session, Identity ownedIdentity) throws SQLException; + void markOwnedIdentityForDeletion(Session session, Identity ownedIdentity) throws SQLException; UID[] getDeviceUidsOfOwnedIdentity(Session session, Identity ownedIdentity) throws SQLException; UID[] getOtherDeviceUidsOfOwnedIdentity(Session session, Identity ownedIdentity) throws SQLException; UID getCurrentDeviceUidOfOwnedIdentity(Session session, Identity ownedIdentity) throws SQLException; - Identity getOwnedIdentityForDeviceUid(Session session, UID currentDeviceUid) throws SQLException; - void addDeviceForOwnedIdentity(Session session, UID deviceUid, Identity ownedIdentity) throws SQLException; - boolean isRemoteDeviceUidOfOwnedIdentity(Session session, UID deviceUid, Identity ownedIdentity) throws SQLException; + Identity getOwnedIdentityForCurrentDeviceUid(Session session, UID currentDeviceUid) throws SQLException; + void addDeviceForOwnedIdentity(Session session, Identity ownedIdentity, UID deviceUid, String displayName, Long expirationTimestamp, Long lastRegistrationTimestamp, boolean channelCreationAlreadyInProgress) throws SQLException; + void updateOwnedDevice(Session session, Identity ownedIdentity, UID deviceUid, String displayName, Long expirationTimestamp, Long lastRegistrationTimestamp) throws SQLException; + void removeDeviceForOwnedIdentity(Session session, Identity ownedIdentity, UID deviceUid) throws SQLException; + List getDevicesOfOwnedIdentity(Session session, Identity ownedIdentity) throws SQLException; + String getCurrentDeviceDisplayName(Session session, Identity ownedIdentity) throws SQLException; void addContactIdentity(Session session, Identity contactIdentity, String serializedDetails, Identity ownedIdentity, TrustOrigin trustOrigin, boolean oneToOne) throws Exception; void addTrustOriginToContact(Session session, Identity contactIdentity, Identity ownedIdentity, TrustOrigin trustOrigin, boolean markContactAsOneToOne) throws SQLException; Identity[] getContactsOfOwnedIdentity(Session session, Identity ownedIdentity); - void trustPublishedContactDetails(Session session, Identity contactIdentity, Identity ownedIdentity) throws SQLException; + JsonIdentityDetailsWithVersionAndPhoto trustPublishedContactDetails(Session session, Identity contactIdentity, Identity ownedIdentity) throws SQLException; void setContactPublishedDetails(Session session, Identity contactIdentity, Identity ownedIdentity, JsonIdentityDetailsWithVersionAndPhoto jsonIdentityDetailsWithVersionAndPhoto, boolean allowDowngrade) throws Exception; void setContactDetailsDownloadedPhoto(Session session, Identity contactIdentity, Identity ownedIdentity, int version, byte[] photo) throws Exception; String getSerializedPublishedDetailsOfContactIdentity(Session session, Identity ownedIdentity, Identity contactIdentity); @@ -138,7 +145,7 @@ public interface IdentityDelegate { boolean reBlockForcefullyUnblockedContact(Session session, Identity ownedIdentity, Identity contactIdentity) throws SQLException; // return true if a device was indeed added, false if the device already existed, and throws an exception if adding the device failed - boolean addDeviceForContactIdentity(Session session, Identity ownedIdentity, Identity contactIdentity, UID deviceUid) throws SQLException; + boolean addDeviceForContactIdentity(Session session, Identity ownedIdentity, Identity contactIdentity, UID deviceUid, boolean channelCreationAlreadyInProgress) throws SQLException; void removeDeviceForContactIdentity(Session session, Identity ownedIdentity, Identity contactIdentity, UID deviceUid) throws SQLException; void removeAllDevicesForContactIdentity(Session session, Identity ownedIdentity, Identity contactIdentity) throws SQLException; UID[] getDeviceUidsOfContactIdentity(Session session, Identity ownedIdentity, Identity contactIdentity) throws SQLException; @@ -159,7 +166,7 @@ public interface IdentityDelegate { byte[] signBlock(Session session, Constants.SignatureContext signatureContext, byte[] block, Identity ownedIdentity, PRNGService prng) throws Exception; byte[] signGroupInvitationNonce(Session session, Constants.SignatureContext signatureContext, GroupV2.Identifier groupIdentifier, byte[] nonce, Identity contactIdentity, Identity ownedIdentity, PRNGService prng) throws Exception; - void createContactGroup(Session session, Identity ownedIdentity, GroupInformation groupInformation, Identity[] groupMembers, IdentityWithSerializedDetails[] pendingGroupMembers) throws Exception; + void createContactGroup(Session session, Identity ownedIdentity, GroupInformation groupInformation, Identity[] groupMembers, IdentityWithSerializedDetails[] pendingGroupMembers, boolean createdByMeOnOtherDevice) throws Exception; void leaveGroup(Session session, byte[] groupOwnerAndUid, Identity ownedIdentity) throws Exception; void addPendingMembersToGroup(Session session, byte[] groupUid, Identity ownedIdentity, Identity[] contactIdentities, GroupMembersChangedCallback groupMembersChangedCallback) throws Exception; void removeMembersAndPendingFromGroup(Session session, byte[] groupOwnerAndUid, Identity ownedIdentity, Identity[] contactIdentities, GroupMembersChangedCallback groupMembersChangedCallback) throws Exception; @@ -176,9 +183,10 @@ public interface IdentityDelegate { GroupInformation getGroupInformation(Session session, Identity ownedIdentity, byte[] groupOwnerAndUid) throws Exception; JsonGroupDetailsWithVersionAndPhoto[] getGroupPublishedAndLatestOrTrustedDetails(Session session, Identity ownedIdentity, byte[] groupOwnerAndUid) throws SQLException; String getGroupPhotoUrl(Session session, Identity ownedIdentity, byte[] groupOwnerAndUid) throws SQLException; - void trustPublishedGroupDetails(Session session, Identity ownedIdentity, byte[] groupOwnerAndUid) throws SQLException; + JsonGroupDetailsWithVersionAndPhoto trustPublishedGroupDetails(Session session, Identity ownedIdentity, byte[] groupOwnerAndUid) throws SQLException; void updateLatestGroupDetails(Session session, Identity ownedIdentity, byte[] groupOwnerAndUid, JsonGroupDetails jsonGroupDetails) throws Exception; void setOwnedGroupDetailsServerLabelAndKey(Session session, Identity ownedIdentity, byte[] groupOwnerAndUid, int version, UID photoServerLabel, AuthEncKey photoServerKey) throws Exception; + void createGroupV1ServerUserData(Session session, Identity ownedIdentity, UID photoServerLabel, byte[] groupOwnerAndUid) throws SQLException; void updateOwnedGroupPhoto(Session session, Identity ownedIdentity, byte[] groupOwnerAndUid, String absolutePhotoUrl, boolean partOfGroupCreation) throws Exception; void setContactGroupDownloadedPhoto(Session session, Identity ownedIdentity, byte[] groupOwnerAndUid, int version, byte[] photo) throws Exception; int publishLatestGroupDetails(Session session, Identity ownedIdentity, byte[] groupOwnerAndUid) throws SQLException; @@ -191,14 +199,15 @@ public interface IdentityDelegate { // region groups v2 - void createNewGroupV2(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier, String serializedGroupDetails, String absolutePhotoUrl, GroupV2.ServerPhotoInfo serverPhotoInfo, byte[] verifiedAdministratorsChain, GroupV2.BlobKeys blobKeys, byte[] ownGroupInvitationNonce, List ownPermissionStrings, HashSet otherGroupMembers) throws Exception; - boolean createJoinedGroupV2(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier, GroupV2.BlobKeys blobKeys, GroupV2.ServerBlob serverBlob) throws Exception; + void createNewGroupV2(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier, String serializedGroupDetails, String absolutePhotoUrl, GroupV2.ServerPhotoInfo serverPhotoInfo, byte[] verifiedAdministratorsChain, GroupV2.BlobKeys blobKeys, byte[] ownGroupInvitationNonce, List ownPermissionStrings, HashSet otherGroupMembers, String serializedGroupType) throws Exception; + boolean createJoinedGroupV2(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier, GroupV2.BlobKeys blobKeys, GroupV2.ServerBlob serverBlob, boolean createdByMeOnOtherDevice) throws Exception; GroupV2.ServerBlob getGroupV2ServerBlob(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws SQLException; String getGroupV2PhotoUrl(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws SQLException; void deleteGroupV2(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws SQLException; void freezeGroupV2(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws SQLException; void unfreezeGroupV2(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws SQLException; Integer getGroupV2Version(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws SQLException; + String getGroupV2JsonGroupType(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws SQLException; boolean isGroupV2Frozen(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws SQLException; GroupV2.BlobKeys getGroupV2BlobKeys(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws SQLException; HashSet getGroupV2OtherMembersAndPermissions(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws Exception; @@ -209,7 +218,7 @@ public interface IdentityDelegate { void moveGroupV2PendingMemberToMembers(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier, Identity groupMemberIdentity) throws Exception; void setGroupV2DownloadedPhoto(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier, GroupV2.ServerPhotoInfo serverPhotoInfo, byte[] photov) throws Exception; ObvGroupV2 getObvGroupV2(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws Exception; - void trustGroupV2PublishedDetails(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws SQLException; + int trustGroupV2PublishedDetails(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws SQLException; GroupV2.ServerPhotoInfo getGroupV2PublishedServerPhotoInfo(Session session, Identity ownedIdentity, byte[] bytesGroupIdentifier); ObvGroupV2.ObvGroupV2DetailsAndPhotos getGroupV2DetailsAndPhotos(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier); void setUpdatedGroupV2PhotoUrl(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier, int version, String absolutePhotoUrl) throws Exception; @@ -229,12 +238,15 @@ public interface IdentityDelegate { void initiateBackup(final BackupDelegate backupDelegate, final String tag, final UID backupKeyUid, final int version); - ObvIdentity[] restoreOwnedIdentitiesFromBackup(String serializedJsonPojo, PRNGService prng); - void restoreContactsAndGroupsFromBackup(String serializedJsonPojo, Identity[] restoredOwnedIdentities, long backupTimestamp); + ObvIdentity[] restoreOwnedIdentitiesFromBackup(String serializedJsonPojo, String deviceDisplayName, PRNGService prng); + void restoreContactsAndGroupsFromBackup(String serializedJsonPojo, ObvIdentity[] restoredOwnedIdentities, long backupTimestamp); // userData UserData[] getAllUserData(Session session) throws Exception; UserData getUserData(Session session, Identity ownedIdentity, UID label) throws Exception; void deleteUserData(Session session, Identity ownedIdentity, UID label) throws Exception; void updateUserDataNextRefreshTimestamp(Session session, Identity ownedIdentity, UID label) throws Exception; + + // device sync + void processSyncItem(Session session, Identity ownedIdentity, ObvSyncAtom obvSyncAtom) throws Exception; } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/NetworkFetchDelegate.java b/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/NetworkFetchDelegate.java index d66ce4d1..af7bef6a 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/NetworkFetchDelegate.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/NetworkFetchDelegate.java @@ -51,9 +51,9 @@ public interface NetworkFetchDelegate { void resendAllDownloadedAttachmentNotifications() throws Exception; void createPendingServerQuery(Session session, ServerQuery serverQuery) throws Exception; - void deleteExistingServerSessionAndCreateANewOne(Session session, Identity identity); + void deleteExistingServerSession(Session session, Identity identity, boolean createNewSession); - void connectWebsockets(String os, String osVersion, int appBuild, String appVersion); + void connectWebsockets(boolean aggressiveReconnectMode, String os, String osVersion, int appBuild, String appVersion); void disconnectWebsockets(); void pingWebsocket (Identity ownedIdentity); byte[] getServerAuthenticationToken(Identity ownedIdentity); diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/PushNotificationDelegate.java b/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/PushNotificationDelegate.java index 7b7b4793..c8645769 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/PushNotificationDelegate.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/PushNotificationDelegate.java @@ -28,8 +28,7 @@ import io.olvid.engine.datatypes.PushNotificationTypeAndParameters; public interface PushNotificationDelegate { - void registerPushNotificationIfConfigurationChanged(Session session, Identity identity, UID deviceUid, PushNotificationTypeAndParameters pushNotificationTypeAndParameters) throws SQLException; - void unregisterPushNotification(Session session, Identity identity) throws SQLException; + void registerPushNotificationIfConfigurationChanged(Session session, Identity ownedIdentity, UID currentDeviceUid, PushNotificationTypeAndParameters pushNotificationTypeAndParameters) throws SQLException; void processAndroidPushNotification(String maskingUidString); void forceRegisterPushNotification(Identity ownedIdentity); Identity getOwnedIdentityFromMaskingUid(String maskingUidString); diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/SolveChallengeDelegate.java b/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/SolveChallengeDelegate.java index da65b84b..8629a036 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/SolveChallengeDelegate.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/metamanager/SolveChallengeDelegate.java @@ -20,12 +20,9 @@ package io.olvid.engine.metamanager; -import java.util.UUID; - import io.olvid.engine.crypto.PRNGService; import io.olvid.engine.datatypes.Identity; public interface SolveChallengeDelegate { byte[] solveChallenge(byte[] challenge, Identity identity, PRNGService prng) throws Exception; - UUID getApiKey(Identity identity); } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/FetchManager.java b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/FetchManager.java index e4f1f339..b4d832e3 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/FetchManager.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/FetchManager.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.sql.SQLException; +import java.util.Objects; import java.util.UUID; import javax.net.ssl.HttpsURLConnection; @@ -74,6 +75,7 @@ import io.olvid.engine.networkfetch.databases.ServerSession; import io.olvid.engine.networkfetch.datatypes.FetchManagerSession; import io.olvid.engine.networkfetch.datatypes.FetchManagerSessionFactory; +import io.olvid.engine.protocol.datatypes.ProtocolStarterDelegate; public class FetchManager implements FetchManagerSessionFactory, NetworkFetchDelegate, PushNotificationDelegate, ObvManager { private final String engineBaseDirectory; @@ -123,6 +125,7 @@ public FetchManager(MetaManager metaManager, SSLSocketFactory sslSocketFactory, metaManager.requestDelegate(this, NotificationPostingDelegate.class); metaManager.requestDelegate(this, ChannelDelegate.class); metaManager.requestDelegate(this, IdentityDelegate.class); + metaManager.requestDelegate(this, ProtocolStarterDelegate.class); metaManager.registerImplementedDelegates(this); } @@ -213,9 +216,13 @@ public void setDelegate(IdentityDelegate identityDelegate) { this.identityDelegate = identityDelegate; } + public void setDelegate(ProtocolStarterDelegate protocolStarterDelegate) { + this.websocketCoordinator.setProtocolStarterDelegate(protocolStarterDelegate); + } + // endregion - public void deleteOwnedIdentity(Session session, Identity ownedIdentity) throws SQLException { + public void deleteOwnedIdentity(Session session, Identity ownedIdentity, boolean doNotDeleteServerSession) throws SQLException { // Delete all InboxMessage (this deletes InboxAttachment) InboxMessage[] inboxMessages = InboxMessage.getAllForOwnedIdentity(wrapSession(session), ownedIdentity); for (InboxMessage inboxMessage: inboxMessages) { @@ -234,7 +241,9 @@ public void deleteOwnedIdentity(Session session, Identity ownedIdentity) throws } } PushNotificationConfiguration.deleteForOwnedIdentity(wrapSession(session), ownedIdentity); - ServerSession.deleteForIdentity(wrapSession(session), ownedIdentity); + if (doNotDeleteServerSession) { + ServerSession.deleteForIdentity(wrapSession(session), ownedIdentity); + } } // region FetchManagerSessionFactory @@ -494,16 +503,22 @@ public void createPendingServerQuery(Session session, ServerQuery serverQuery) t } - public void deleteExistingServerSessionAndCreateANewOne(Session session, Identity ownedIdentity) { + public void deleteExistingServerSession(Session session, Identity ownedIdentity, boolean createNewSession) { ServerSession.deleteForIdentity(wrapSession(session), ownedIdentity); + if (createNewSession) { + createServerSessionCoordinator.createServerSession(ownedIdentity); + } + } + + public void createServerSession(Identity ownedIdentity) { createServerSessionCoordinator.createServerSession(ownedIdentity); } - public void connectWebsockets(String os, String osVersion, int appBuild, String appVersion) { + public void connectWebsockets(boolean relyOnWebsocketForNetworkDetection, String os, String osVersion, int appBuild, String appVersion) { if ("javax.net.ssl.HttpsURLConnection.DefaultHostnameVerifier".equals(HttpsURLConnection.getDefaultHostnameVerifier().getClass().getCanonicalName())) { Logger.w("WARNING: default HostnameVerifier not set. Websocket connection will most probably fail.\n\tYou may want to consider using OkHttp's HostnameVerifier as the default with:\n\t\tHttpsURLConnection.setDefaultHostnameVerifier(OkHostnameVerifier.INSTANCE);"); } - websocketCoordinator.connectWebsockets(os, osVersion, appBuild, appVersion); + websocketCoordinator.connectWebsockets(relyOnWebsocketForNetworkDetection, os, osVersion, appBuild, appVersion); } public void disconnectWebsockets() { @@ -590,45 +605,36 @@ public String getAddressServerUrl(String server) { // region implement PushNotificationDelegate @Override - public void registerPushNotificationIfConfigurationChanged(Session session, Identity identity, UID deviceUid, PushNotificationTypeAndParameters newPushParameters) throws SQLException { + public void registerPushNotificationIfConfigurationChanged(Session session, Identity ownedIdentity, UID currentDeviceUid, PushNotificationTypeAndParameters newPushParameters) throws SQLException { FetchManagerSession fetchManagerSession = wrapSession(session); - PushNotificationConfiguration pushNotificationConfiguration = PushNotificationConfiguration.get(fetchManagerSession, identity); + PushNotificationConfiguration pushNotificationConfiguration = PushNotificationConfiguration.get(fetchManagerSession, ownedIdentity); if (pushNotificationConfiguration != null) { - if (pushNotificationConfiguration.getDeviceUid().equals(deviceUid)) { + if (pushNotificationConfiguration.getDeviceUid().equals(currentDeviceUid)) { PushNotificationTypeAndParameters oldPushParameters = pushNotificationConfiguration.getPushNotificationTypeAndParameters(); - if (oldPushParameters.equals(newPushParameters)) { - // when parameters are equal, we only replace them in DB if it changed from a no-kick to a kick - if (oldPushParameters.kickOtherDevices || !newPushParameters.kickOtherDevices) { - return; - } else { - // we are simply going into kick mode, no other change, so no need to change the maskingUid + if (oldPushParameters.sameTypeAndToken(newPushParameters)) { + // when parameters are equal, we only replace them in DB if it changed from a no-reactivate to a reactivate, or if the deviceUidToReplace has changed + if ((!oldPushParameters.reactivateCurrentDevice && newPushParameters.reactivateCurrentDevice) || (oldPushParameters.reactivateCurrentDevice && newPushParameters.reactivateCurrentDevice && !Objects.equals(oldPushParameters.deviceUidToReplace, newPushParameters.deviceUidToReplace))) { + // tokens are the same, no need to change identityMaskingUid newPushParameters.identityMaskingUid = oldPushParameters.identityMaskingUid; + } else { + return; } } else { // token has changed, or notification type has changed - // we still need to preserve the kickOtherDevices parameter - if (oldPushParameters.kickOtherDevices) { - newPushParameters.kickOtherDevices = true; + // we still need to preserve the reactivateCurrentDevice and deviceUidToReplace parameters + if (oldPushParameters.reactivateCurrentDevice && !newPushParameters.reactivateCurrentDevice) { + newPushParameters.reactivateCurrentDevice = true; + newPushParameters.deviceUidToReplace = oldPushParameters.deviceUidToReplace; } } } pushNotificationConfiguration.delete(); } - if (PushNotificationConfiguration.create(fetchManagerSession, identity, deviceUid, newPushParameters) == null) { + if (PushNotificationConfiguration.create(fetchManagerSession, ownedIdentity, currentDeviceUid, newPushParameters) == null) { throw new SQLException(); } } - @Override - public void unregisterPushNotification(Session session, Identity identity) throws SQLException { - FetchManagerSession fetchManagerSession = wrapSession(session); - PushNotificationConfiguration pushNotificationConfiguration = PushNotificationConfiguration.get(fetchManagerSession, identity); - if (pushNotificationConfiguration != null) { - pushNotificationConfiguration.delete(); - } - } - - @Override public void processAndroidPushNotification(String maskingUidString) { serverPushNotificationsCoordinator.processAndroidPushNotification(maskingUidString); diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/RegisterServerPushNotificationsCoordinator.java b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/RegisterServerPushNotificationsCoordinator.java index 10a0d03d..acf69e32 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/RegisterServerPushNotificationsCoordinator.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/RegisterServerPushNotificationsCoordinator.java @@ -119,7 +119,10 @@ public void initialQueueing() { storeAndroidIdentityMaskingUid(pushNotificationConfiguration.getOwnedIdentity(), pushNotificationConfiguration.getDeviceUid(), pushNotificationConfiguration.getIdentityMaskingUid()); registerServerPushNotification(pushNotificationConfiguration.getOwnedIdentity()); break; - case PushNotificationTypeAndParameters.PUSH_NOTIFICATION_TYPE_NONE: + case PushNotificationTypeAndParameters.PUSH_NOTIFICATION_TYPE_WEBSOCKET_ANDROID: + case PushNotificationTypeAndParameters.PUSH_NOTIFICATION_TYPE_WEBSOCKET_WINDOWS: + case PushNotificationTypeAndParameters.PUSH_NOTIFICATION_TYPE_WEBSOCKET_LINUX: + case PushNotificationTypeAndParameters.PUSH_NOTIFICATION_TYPE_WEBSOCKET_DAEMON: registerServerPushNotification(pushNotificationConfiguration.getOwnedIdentity()); break; } @@ -163,7 +166,7 @@ public void onFinishCallback(Operation operation) { @Override public void onCancelCallback(Operation operation) { - Identity identity = ((RegisterPushNotificationOperation) operation).getOwnedIdentity(); + Identity ownedIdentity = ((RegisterPushNotificationOperation) operation).getOwnedIdentity(); Integer rfc = operation.getReasonForCancel(); Logger.i("RegisterPushNotificationOperation cancelled for reason " + rfc); if (rfc == null) { @@ -171,15 +174,22 @@ public void onCancelCallback(Operation operation) { } switch (rfc) { case RegisterPushNotificationOperation.RFC_INVALID_SERVER_SESSION: { - waitForServerSession(identity); - createServerSessionDelegate.createServerSession(identity); + waitForServerSession(ownedIdentity); + createServerSessionDelegate.createServerSession(ownedIdentity); + break; + } + case RegisterPushNotificationOperation.RFC_ANOTHER_DEVICE_IS_ALREADY_REGISTERED: + case RegisterPushNotificationOperation.RFC_PUSH_NOTIFICATION_CONFIGURATION_NOT_FOUND: { break; } - case RegisterPushNotificationOperation.RFC_ANOTHER_DEVICE_IS_ALREADY_REGISTERED: { + case RegisterPushNotificationOperation.RFC_DEVICE_UID_TO_REPLACE_NOT_FOUND: { + HashMap userInfo = new HashMap<>(); + userInfo.put(DownloadNotifications.NOTIFICATION_PUSH_REGISTER_FAILED_BAD_DEVICE_UID_TO_REPLACE_OWNED_IDENTITY_KEY, ownedIdentity); + notificationPostingDelegate.postNotification(DownloadNotifications.NOTIFICATION_PUSH_REGISTER_FAILED_BAD_DEVICE_UID_TO_REPLACE, userInfo); break; } default: { - scheduleNewRegisterPushNotificationOperationQueueing(identity); + scheduleNewRegisterPushNotificationOperationQueueing(ownedIdentity); } } } @@ -202,7 +212,10 @@ public void newPushNotificationConfiguration(Identity identity, UID deviceUid, P storeAndroidIdentityMaskingUid(identity, deviceUid, pushNotificationTypeAndParameters.identityMaskingUid); registerServerPushNotification(identity); break; - case PushNotificationTypeAndParameters.PUSH_NOTIFICATION_TYPE_NONE: + case PushNotificationTypeAndParameters.PUSH_NOTIFICATION_TYPE_WEBSOCKET_ANDROID: + case PushNotificationTypeAndParameters.PUSH_NOTIFICATION_TYPE_WEBSOCKET_WINDOWS: + case PushNotificationTypeAndParameters.PUSH_NOTIFICATION_TYPE_WEBSOCKET_LINUX: + case PushNotificationTypeAndParameters.PUSH_NOTIFICATION_TYPE_WEBSOCKET_DAEMON: registerServerPushNotification(identity); break; } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/ServerQueryCoordinator.java b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/ServerQueryCoordinator.java index b8a88271..78afff64 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/ServerQueryCoordinator.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/ServerQueryCoordinator.java @@ -81,7 +81,6 @@ public ServerQueryCoordinator(FetchManagerSessionFactory fetchManagerSessionFact this.serverUserDataCoordinator = serverUserDataCoordinator; serverQueriesOperationQueue = new NoDuplicateOperationQueue(); - serverQueriesOperationQueue.execute(1, "Engine-ServerQueryCoordinator"); scheduler = new ExponentialBackoffRepeatingScheduler<>(); @@ -109,6 +108,8 @@ public void initialQueueing() { e.printStackTrace(); } } + // only start processing queries after the initial queueing is performed (otherwise a query could be queued while its already being executed) + serverQueriesOperationQueue.execute(1, "Engine-ServerQueryCoordinator"); } public void setNotificationListeningDelegate(NotificationListeningDelegate notificationListeningDelegate) { @@ -210,8 +211,10 @@ public void onCancelCallback(Operation operation) { rfc = Operation.RFC_NULL; } switch (rfc) { + case ServerQueryOperation.RFC_MALFORMED_URL: case ServerQueryOperation.RFC_USER_DATA_TOO_LARGE: - case ServerQueryOperation.RFC_BAD_ENCODED_SERVER_QUERY: { + case ServerQueryOperation.RFC_BAD_ENCODED_SERVER_QUERY: + case ServerQueryOperation.RFC_DEVICE_DOES_NOT_EXIST: { // PendingServerQuery cannot be understood, // or the data to send is too large // ==> we can delete it from the database @@ -227,20 +230,19 @@ public void onCancelCallback(Operation operation) { break; } case ServerQueryOperation.RFC_INVALID_SERVER_SESSION: { - if (serverQuery.getType().getId() == ServerQuery.Type.PUT_USER_DATA_QUERY_ID - || serverQuery.getType().getId() == ServerQuery.Type.CREATE_GROUP_BLOB_QUERY_ID) { - waitForServerSession(serverQuery.getOwnedIdentity(), serverQueryUid); - createServerSessionDelegate.createServerSession(serverQuery.getOwnedIdentity()); - } + waitForServerSession(serverQuery.getOwnedIdentity(), serverQueryUid); + createServerSessionDelegate.createServerSession(serverQuery.getOwnedIdentity()); break; } case ServerQueryOperation.RFC_IDENTITY_IS_INACTIVE: { waitForIdentityReactivation(serverQuery.getOwnedIdentity(), serverQueryUid); break; } - default: + case ServerQueryOperation.RFC_DEVICE_NOT_YET_REGISTERED: + default: { // Requeue the operation in the future scheduleNewServerQueryOperation(serverQueryUid); + } } } @@ -289,7 +291,7 @@ public void onFinishCallback(Operation operation) { } if (serverQuery.getType().getId() == ServerQuery.Type.PUT_USER_DATA_QUERY_ID) { - serverUserDataCoordinator.newUserDataUploaded(serverQuery.getOwnedIdentity(), serverQuery.getType().getServerLabel()); + serverUserDataCoordinator.newUserDataUploaded(serverQuery.getOwnedIdentity(), serverQuery.getType().getServerLabelOrDeviceUid()); } } catch (Exception e) { e.printStackTrace(); diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/WebsocketCoordinator.java b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/WebsocketCoordinator.java index bd3757ff..81501acc 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/WebsocketCoordinator.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/coordinators/WebsocketCoordinator.java @@ -64,6 +64,7 @@ import io.olvid.engine.networkfetch.datatypes.FetchManagerSession; import io.olvid.engine.networkfetch.datatypes.FetchManagerSessionFactory; import io.olvid.engine.networkfetch.datatypes.WellKnownCacheDelegate; +import io.olvid.engine.protocol.datatypes.ProtocolStarterDelegate; import okhttp3.Authenticator; import okhttp3.Interceptor; import okhttp3.OkHttpClient; @@ -105,10 +106,12 @@ public class WebsocketCoordinator implements Operation.OnCancelCallback { @SuppressWarnings("FieldCanBeLocal") private NotificationListeningDelegate notificationListeningDelegate; private NotificationPostingDelegate notificationPostingDelegate; + private ProtocolStarterDelegate protocolStarterDelegate; private final OkHttpClient okHttpClient; private boolean doConnect = false; + private boolean relyOnWebsocketForNetworkDetection = false; private String os; private String osVersion; @@ -134,7 +137,16 @@ public WebsocketCoordinator(FetchManagerSessionFactory fetchManagerSessionFactor identityRegistrationOperationQueue = new NoDuplicateOperationQueue(); identityRegistrationOperationQueue.execute(1, "Engine-WebsocketCoordinator-register"); - scheduler = new ExponentialBackoffRepeatingScheduler<>(); + scheduler = new ExponentialBackoffRepeatingScheduler<>() { + @Override + protected long computeReschedulingDelay(int failedAttemptCount) { + // if in aggressive mode (for Desktop), never reschedule too far in the future + if (relyOnWebsocketForNetworkDetection) { + return Math.min(super.computeReschedulingDelay(failedAttemptCount), Constants.WEBSOCKET_PING_INTERVAL_MILLIS); + } + return super.computeReschedulingDelay(failedAttemptCount); + } + }; awaitingServerSessionIdentities = new HashSet<>(); awaitingServerSessionIdentitiesLock = new Object(); @@ -210,6 +222,11 @@ public void setNotificationPostingDelegate(NotificationPostingDelegate notificat this.notificationPostingDelegate = notificationPostingDelegate; } + + public void setProtocolStarterDelegate(ProtocolStarterDelegate protocolStarterDelegate) { + this.protocolStarterDelegate = protocolStarterDelegate; + } + public void initialQueueing() { synchronized (ownedIdentityAndUidsLock) { if (initialQueueingPerformed) { @@ -241,8 +258,9 @@ public void initialQueueing() { resetWebsockets(); } - public void connectWebsockets(String os, String osVersion, int appBuild, String appVersion) { - doConnect = true; + public void connectWebsockets(boolean relyOnWebsocketForNetworkDetection, String os, String osVersion, int appBuild, String appVersion) { + this.doConnect = true; + this.relyOnWebsocketForNetworkDetection = relyOnWebsocketForNetworkDetection; this.os = os; this.osVersion = osVersion; this.appBuild = appBuild; @@ -252,7 +270,8 @@ public void connectWebsockets(String os, String osVersion, int appBuild, String } public void disconnectWebsockets() { - doConnect = false; + this.doConnect = false; + this.relyOnWebsocketForNetworkDetection = false; internalDisconnectWebsockets(); } @@ -630,6 +649,9 @@ public void onOpen(WebSocket webSocket, Response response) { HashMap userInfo = new HashMap<>(); userInfo.put(DownloadNotifications.NOTIFICATION_WEBSOCKET_CONNECTION_STATE_CHANGED_STATE_KEY, 1); notificationPostingDelegate.postNotification(DownloadNotifications.NOTIFICATION_WEBSOCKET_CONNECTION_STATE_CHANGED, userInfo); + if (relyOnWebsocketForNetworkDetection) { + notificationPostingDelegate.postNotification(DownloadNotifications.NOTIFICATION_WEBSOCKET_DETECTED_SOME_NETWORK, new HashMap<>()); + } } List identityAndUids = ownedIdentityAndUidsByServer.get(server); @@ -845,6 +867,27 @@ public void onMessage(WebSocket webSocket, String message) { } break; } + case "ownedDevices": { + Object identityObject = receivedMessage.get("identity"); + if (!(identityObject instanceof String)) { + break; + } + try { + Identity identity = Identity.of(Base64.decode((String) identityObject)); + + if (protocolStarterDelegate != null) { + try { + protocolStarterDelegate.startOwnedDeviceDiscoveryProtocol(identity); + } catch (Exception e) { + e.printStackTrace(); + } + } + } catch (IOException | DecodingException e) { + Logger.d("Error decoding identity in ownedDevices websocket notification"); + e.printStackTrace(); + } + break; + } } } } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/databases/PushNotificationConfiguration.java b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/databases/PushNotificationConfiguration.java index f057ec3e..9bb3d949 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/databases/PushNotificationConfiguration.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/databases/PushNotificationConfiguration.java @@ -54,9 +54,12 @@ public class PushNotificationConfiguration implements ObvDatabase { static final String IDENTITY_MASKING_UID = "identity_masking_uid"; private int multiDeviceConfiguration; static final String MULTI_DEVICE_CONFIGURATION = "multi_device_configuration"; + private UID deviceUidToReplace; + static final String DEVICE_UID_TO_REPLACE = "device_uid_to_replace"; - private static final int CONFIGURATION_KICK_OTHER_DEVICES_AT_NEXT_REGISTRATION_BIT = 0x1; - private static final int CONFIGURATION_USE_MULTI_DEVICE_BIT = 0x2; + + private static final int CONFIGURATION_REACTIVATE_CURRENT_DEVICE_AT_NEXT_REGISTRATION_BIT = 0x1; +// private static final int CONFIGURATION_USE_MULTI_DEVICE_BIT = 0x2; public Identity getOwnedIdentity() { return ownedIdentity; @@ -78,18 +81,17 @@ public UID getIdentityMaskingUid() { return identityMaskingUid; } - public boolean shouldKickOtherDevices() { - return (multiDeviceConfiguration & CONFIGURATION_KICK_OTHER_DEVICES_AT_NEXT_REGISTRATION_BIT) != 0; + public boolean shouldReactivateCurrentDevice() { + return (multiDeviceConfiguration & CONFIGURATION_REACTIVATE_CURRENT_DEVICE_AT_NEXT_REGISTRATION_BIT) != 0; } - public boolean useMultiDevice() { - return (multiDeviceConfiguration & CONFIGURATION_USE_MULTI_DEVICE_BIT) != 0; + public UID getDeviceUidToReplace() { + return deviceUidToReplace; } public PushNotificationTypeAndParameters getPushNotificationTypeAndParameters() { - boolean kickOtherDevices = (multiDeviceConfiguration & CONFIGURATION_KICK_OTHER_DEVICES_AT_NEXT_REGISTRATION_BIT) != 0; - boolean useMultiDevice = (multiDeviceConfiguration & CONFIGURATION_USE_MULTI_DEVICE_BIT) != 0; - return new PushNotificationTypeAndParameters(pushNotificationType, token, identityMaskingUid, kickOtherDevices, useMultiDevice); + boolean reactivateCurrentDevice = (multiDeviceConfiguration & CONFIGURATION_REACTIVATE_CURRENT_DEVICE_AT_NEXT_REGISTRATION_BIT) != 0; + return new PushNotificationTypeAndParameters(pushNotificationType, token, identityMaskingUid, reactivateCurrentDevice, deviceUidToReplace); } // region constructors @@ -100,15 +102,12 @@ public static PushNotificationConfiguration create(FetchManagerSession fetchMana } int multiDeviceConfiguration = 0; - if (pushNotificationTypeAndParameters.kickOtherDevices) { - multiDeviceConfiguration |= CONFIGURATION_KICK_OTHER_DEVICES_AT_NEXT_REGISTRATION_BIT; - } - if (pushNotificationTypeAndParameters.useMultiDevice) { - multiDeviceConfiguration |= CONFIGURATION_USE_MULTI_DEVICE_BIT; + if (pushNotificationTypeAndParameters.reactivateCurrentDevice) { + multiDeviceConfiguration |= CONFIGURATION_REACTIVATE_CURRENT_DEVICE_AT_NEXT_REGISTRATION_BIT; } try { - PushNotificationConfiguration pushNotificationConfiguration = new PushNotificationConfiguration(fetchManagerSession, ownedIdentity, deviceUid, pushNotificationTypeAndParameters.pushNotificationType, pushNotificationTypeAndParameters.token, pushNotificationTypeAndParameters.identityMaskingUid, multiDeviceConfiguration); + PushNotificationConfiguration pushNotificationConfiguration = new PushNotificationConfiguration(fetchManagerSession, ownedIdentity, deviceUid, pushNotificationTypeAndParameters.pushNotificationType, pushNotificationTypeAndParameters.token, pushNotificationTypeAndParameters.identityMaskingUid, multiDeviceConfiguration, pushNotificationTypeAndParameters.deviceUidToReplace); pushNotificationConfiguration.insert(); return pushNotificationConfiguration; } catch (SQLException e) { @@ -117,7 +116,7 @@ public static PushNotificationConfiguration create(FetchManagerSession fetchMana } } - private PushNotificationConfiguration(FetchManagerSession fetchManagerSession, Identity ownedIdentity, UID deviceUid, byte pushNotificationType, byte[] token, UID identityMaskingUid, int multiDeviceConfiguration) { + private PushNotificationConfiguration(FetchManagerSession fetchManagerSession, Identity ownedIdentity, UID deviceUid, byte pushNotificationType, byte[] token, UID identityMaskingUid, int multiDeviceConfiguration, UID deviceUidToReplace) { this.fetchManagerSession = fetchManagerSession; this.ownedIdentity = ownedIdentity; this.deviceUid = deviceUid; @@ -125,6 +124,7 @@ private PushNotificationConfiguration(FetchManagerSession fetchManagerSession, I this.token = token; this.identityMaskingUid = identityMaskingUid; this.multiDeviceConfiguration = multiDeviceConfiguration; + this.deviceUidToReplace = deviceUidToReplace; } private PushNotificationConfiguration(FetchManagerSession fetchManagerSession, ResultSet res) throws SQLException { @@ -138,8 +138,10 @@ private PushNotificationConfiguration(FetchManagerSession fetchManagerSession, R this.pushNotificationType = res.getByte(PUSH_NOTIFICATION_TYPE); this.token = res.getBytes(TOKEN); byte[] identityMaskingBytes = res.getBytes(IDENTITY_MASKING_UID); - this.identityMaskingUid = (identityMaskingBytes==null)?null:new UID(identityMaskingBytes); + this.identityMaskingUid = (identityMaskingBytes == null) ? null : new UID(identityMaskingBytes); this.multiDeviceConfiguration = res.getInt(MULTI_DEVICE_CONFIGURATION); + byte[] bytesDeviceUidToReplace = res.getBytes(DEVICE_UID_TO_REPLACE); + this.deviceUidToReplace = (bytesDeviceUidToReplace == null) ? null : new UID(bytesDeviceUidToReplace); } // endregion @@ -154,7 +156,9 @@ public static void createTable(Session session) throws SQLException { PUSH_NOTIFICATION_TYPE + " INT NOT NULL, " + TOKEN + " BLOB, " + IDENTITY_MASKING_UID + " BLOB, " + - MULTI_DEVICE_CONFIGURATION + " INT NOT NULL);"); + MULTI_DEVICE_CONFIGURATION + " INT NOT NULL," + + DEVICE_UID_TO_REPLACE + " BLOB " + + ");"); } } @@ -164,18 +168,27 @@ public static void upgradeTable(Session session, int oldVersion, int newVersion) try (Statement statement = session.createStatement()) { statement.execute("DROP TABLE IF EXISTS registered_push_notification"); } + oldVersion = 15; + } + if (oldVersion < 35 && newVersion >= 35) { + try (Statement statement = session.createStatement()) { + Logger.d("MIGRATING `push_notification_configuration` TABLE FROM VERSION " + oldVersion + " TO 35"); + statement.execute("ALTER TABLE push_notification_configuration ADD COLUMN device_uid_to_replace BLOB DEFAULT NULL"); + } + oldVersion = 35; } } @Override public void insert() throws SQLException { - try (PreparedStatement statement = fetchManagerSession.session.prepareStatement("INSERT INTO " + TABLE_NAME + " VALUES(?,?,?,?,?, ?);")) { + try (PreparedStatement statement = fetchManagerSession.session.prepareStatement("INSERT INTO " + TABLE_NAME + " VALUES(?,?,?,?,?, ?,?);")) { statement.setBytes(1, ownedIdentity.getBytes()); statement.setBytes(2, deviceUid.getBytes()); statement.setByte(3, pushNotificationType); statement.setBytes(4, token); - statement.setBytes(5, (identityMaskingUid==null)?null:identityMaskingUid.getBytes()); + statement.setBytes(5, (identityMaskingUid == null) ? null : identityMaskingUid.getBytes()); statement.setInt(6, multiDeviceConfiguration); + statement.setBytes(7, (deviceUidToReplace == null) ? null : deviceUidToReplace.getBytes()); statement.executeUpdate(); this.commitHookBits |= HOOK_BIT_INSERT; fetchManagerSession.session.addSessionCommitListener(this); @@ -233,9 +246,10 @@ public static PushNotificationConfiguration[] getAll(FetchManagerSession fetchMa public void clearKickOtherDevices() { try (PreparedStatement statement = fetchManagerSession.session.prepareStatement("UPDATE " + TABLE_NAME + - " SET " + MULTI_DEVICE_CONFIGURATION + " = ? " + + " SET " + MULTI_DEVICE_CONFIGURATION + " = ?, " + + DEVICE_UID_TO_REPLACE + " = NULL " + " WHERE " + OWNED_IDENTITY + " = ?;")) { - int newMultiDeviceConfiguration = multiDeviceConfiguration & (~CONFIGURATION_KICK_OTHER_DEVICES_AT_NEXT_REGISTRATION_BIT); + int newMultiDeviceConfiguration = multiDeviceConfiguration & (~CONFIGURATION_REACTIVATE_CURRENT_DEVICE_AT_NEXT_REGISTRATION_BIT); statement.setInt(1, newMultiDeviceConfiguration); statement.setBytes(2, ownedIdentity.getBytes()); statement.executeUpdate(); diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/databases/ServerSession.java b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/databases/ServerSession.java index 4f0720f6..c5c37cb7 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/databases/ServerSession.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/databases/ServerSession.java @@ -42,6 +42,7 @@ public class ServerSession implements ObvDatabase { public enum Permission { CALL, WEB_CLIENT, + MULTI_DEVICE, } public enum ApiKeyStatus { @@ -135,6 +136,9 @@ public static List deserializePermissions(long permissions) { if ((permissions & Constants.API_KEY_PERMISSION_WEB_CLIENT) != 0){ out.add(Permission.WEB_CLIENT); } + if ((permissions & Constants.API_KEY_PERMISSION_MULTI_DEVICE) != 0){ + out.add(Permission.MULTI_DEVICE); + } return out; } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/CreateServerSessionCompositeOperation.java b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/CreateServerSessionCompositeOperation.java index 4b49649d..5dd42c92 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/CreateServerSessionCompositeOperation.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/CreateServerSessionCompositeOperation.java @@ -53,7 +53,7 @@ public CreateServerSessionCompositeOperation(FetchManagerSessionFactory fetchMan this.ownedIdentity = ownedIdentity; this.suboperations = new Operation[3]; - suboperations[0] = new RequestChallengeOperation(fetchManagerSessionFactory, sslSocketFactory, ownedIdentity, solveChallengeDelegate); + suboperations[0] = new RequestChallengeOperation(fetchManagerSessionFactory, sslSocketFactory, ownedIdentity); suboperations[1] = new SolveChallengeOperation(fetchManagerSessionFactory, ownedIdentity, solveChallengeDelegate); suboperations[2] = new GetTokenOperation(fetchManagerSessionFactory, sslSocketFactory, ownedIdentity, this); diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/FreeTrialOperation.java b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/FreeTrialOperation.java index 53d88379..bf1a8215 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/FreeTrialOperation.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/FreeTrialOperation.java @@ -79,7 +79,6 @@ public void doExecute() { userInfo.put(DownloadNotifications.NOTIFICATION_FREE_TRIAL_RETRIEVE_SUCCESS_OWNED_IDENTITY_KEY, ownedIdentity); fetchManagerSession.notificationPostingDelegate.postNotification(DownloadNotifications.NOTIFICATION_FREE_TRIAL_RETRIEVE_SUCCESS, userInfo); - fetchManagerSession.identityDelegate.updateApiKeyOfOwnedIdentity(fetchManagerSession.session, ownedIdentity, serverMethod.getApiKey()); ServerSession.deleteForIdentity(fetchManagerSession, ownedIdentity); fetchManagerSession.createServerSessionDelegate.createServerSession(ownedIdentity); } else { diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/RegisterPushNotificationOperation.java b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/RegisterPushNotificationOperation.java index 7e3083f3..3d332bd1 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/RegisterPushNotificationOperation.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/RegisterPushNotificationOperation.java @@ -19,11 +19,14 @@ package io.olvid.engine.networkfetch.operations; +import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.util.List; import javax.net.ssl.SSLSocketFactory; +import io.olvid.engine.crypto.Suite; +import io.olvid.engine.datatypes.EncryptedBytes; import io.olvid.engine.datatypes.Identity; import io.olvid.engine.datatypes.Operation; import io.olvid.engine.datatypes.ServerMethod; @@ -41,6 +44,8 @@ public class RegisterPushNotificationOperation extends Operation { public static final int RFC_NETWORK_ERROR = 1; public static final int RFC_INVALID_SERVER_SESSION = 2; public static final int RFC_ANOTHER_DEVICE_IS_ALREADY_REGISTERED = 3; + public static final int RFC_PUSH_NOTIFICATION_CONFIGURATION_NOT_FOUND = 4; + public static final int RFC_DEVICE_UID_TO_REPLACE_NOT_FOUND = 5; private final FetchManagerSessionFactory fetchManagerSessionFactory; @@ -84,15 +89,33 @@ public void doExecute() { return; } PushNotificationConfiguration pushNotificationConfiguration = PushNotificationConfiguration.get(fetchManagerSession, ownedIdentity); + if (pushNotificationConfiguration == null) { + cancel(RFC_PUSH_NOTIFICATION_CONFIGURATION_NOT_FOUND); + return; + } this.deviceUid = pushNotificationConfiguration.getDeviceUid(); + String deviceName = fetchManagerSession.identityDelegate.getCurrentDeviceDisplayName(fetchManagerSession.session, ownedIdentity); + byte[] encodedDeviceName = Encoded.of(new Encoded[]{ + Encoded.of(deviceName == null ? new byte[0] : deviceName.getBytes(StandardCharsets.UTF_8)) + }).getBytes(); + + byte[] plaintext = new byte[((encodedDeviceName.length - 1) | 127) + 1]; + System.arraycopy(encodedDeviceName, 0, plaintext, 0, encodedDeviceName.length); + + EncryptedBytes encryptedDeviceNameForFirstRegistration = Suite.getPublicKeyEncryption(ownedIdentity.getEncryptionPublicKey()).encrypt( + ownedIdentity.getEncryptionPublicKey(), + plaintext, + Suite.getDefaultPRNGService(0)); + List keycloakPushTopics = fetchManagerSession.identityDelegate.getKeycloakPushTopics(fetchManagerSession.session, ownedIdentity); RegisterPushNotificationServerMethod serverMethod = new RegisterPushNotificationServerMethod( ownedIdentity, serverSessionToken, pushNotificationConfiguration.getDeviceUid(), pushNotificationConfiguration.getPushNotificationTypeAndParameters(), - keycloakPushTopics + keycloakPushTopics, + encryptedDeviceNameForFirstRegistration ); serverMethod.setSslSocketFactory(sslSocketFactory); @@ -102,7 +125,7 @@ public void doExecute() { switch (returnStatus) { case ServerMethod.OK: fetchManagerSession.identityDelegate.reactivateOwnedIdentityIfNeeded(fetchManagerSession.session, ownedIdentity); - if (pushNotificationConfiguration.shouldKickOtherDevices()) { + if (pushNotificationConfiguration.shouldReactivateCurrentDevice()) { pushNotificationConfiguration.clearKickOtherDevices(); } finished = true; @@ -117,6 +140,14 @@ public void doExecute() { fetchManagerSession.session.commit(); cancel(RFC_ANOTHER_DEVICE_IS_ALREADY_REGISTERED); return; + case ServerMethod.DEVICE_IS_NOT_REGISTERED: + // this only happens when configuration was set to reactivate curent device, but the provided deviceUidToReplace is invalid/inactive + if (pushNotificationConfiguration.shouldReactivateCurrentDevice()) { + pushNotificationConfiguration.clearKickOtherDevices(); + fetchManagerSession.session.commit(); + } + cancel(RFC_DEVICE_UID_TO_REPLACE_NOT_FOUND); + return; default: cancel(RFC_NETWORK_ERROR); return; @@ -151,14 +182,16 @@ class RegisterPushNotificationServerMethod extends ServerMethod { private final UID deviceUid; private final PushNotificationTypeAndParameters pushNotificationTypeAndParameters; private final String[] keycloakPushTopics; + private final EncryptedBytes encryptedDeviceNameForFirstRegistration; - RegisterPushNotificationServerMethod(Identity ownedIdentity, byte[] token, UID deviceUid, PushNotificationTypeAndParameters pushNotificationTypeAndParameters, List keycloakPushTopics) { + RegisterPushNotificationServerMethod(Identity ownedIdentity, byte[] token, UID deviceUid, PushNotificationTypeAndParameters pushNotificationTypeAndParameters, List keycloakPushTopics, EncryptedBytes encryptedDeviceNameForFirstRegistration) { this.server = ownedIdentity.getServer(); this.ownedIdentity = ownedIdentity; this.token = token; this.deviceUid = deviceUid; this.pushNotificationTypeAndParameters = pushNotificationTypeAndParameters; this.keycloakPushTopics = keycloakPushTopics == null ? new String[0] : keycloakPushTopics.toArray(new String[0]); + this.encryptedDeviceNameForFirstRegistration = encryptedDeviceNameForFirstRegistration; } @Override @@ -182,23 +215,40 @@ protected byte[] getDataToSend() { }); break; } - case PushNotificationTypeAndParameters.PUSH_NOTIFICATION_TYPE_NONE: + case PushNotificationTypeAndParameters.PUSH_NOTIFICATION_TYPE_WEBSOCKET_ANDROID: + case PushNotificationTypeAndParameters.PUSH_NOTIFICATION_TYPE_WEBSOCKET_WINDOWS: + case PushNotificationTypeAndParameters.PUSH_NOTIFICATION_TYPE_WEBSOCKET_LINUX: + case PushNotificationTypeAndParameters.PUSH_NOTIFICATION_TYPE_WEBSOCKET_DAEMON: default: { extraInfo = Encoded.of(new Encoded[0]); break; } } - return Encoded.of(new Encoded[]{ - Encoded.of(ownedIdentity), - Encoded.of(token), - Encoded.of(deviceUid), - Encoded.of(new byte[]{pushNotificationTypeAndParameters.pushNotificationType}), - extraInfo, - Encoded.of(pushNotificationTypeAndParameters.kickOtherDevices), - Encoded.of(pushNotificationTypeAndParameters.useMultiDevice), - Encoded.of(keycloakPushTopics) - }).getBytes(); + if (pushNotificationTypeAndParameters.deviceUidToReplace != null) { + return Encoded.of(new Encoded[]{ + Encoded.of(ownedIdentity), + Encoded.of(token), + Encoded.of(deviceUid), + Encoded.of(new byte[]{pushNotificationTypeAndParameters.pushNotificationType}), + extraInfo, + Encoded.of(pushNotificationTypeAndParameters.reactivateCurrentDevice), + Encoded.of(keycloakPushTopics), + Encoded.of(encryptedDeviceNameForFirstRegistration), + Encoded.of(pushNotificationTypeAndParameters.deviceUidToReplace), + }).getBytes(); + } else { + return Encoded.of(new Encoded[]{ + Encoded.of(ownedIdentity), + Encoded.of(token), + Encoded.of(deviceUid), + Encoded.of(new byte[]{pushNotificationTypeAndParameters.pushNotificationType}), + extraInfo, + Encoded.of(pushNotificationTypeAndParameters.reactivateCurrentDevice), + Encoded.of(keycloakPushTopics), + Encoded.of(encryptedDeviceNameForFirstRegistration), + }).getBytes(); + } } @Override diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/RequestChallengeOperation.java b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/RequestChallengeOperation.java index 75612956..d1ed0761 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/RequestChallengeOperation.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/RequestChallengeOperation.java @@ -22,7 +22,6 @@ import java.sql.SQLException; import java.util.Arrays; -import java.util.UUID; import javax.net.ssl.SSLSocketFactory; @@ -35,7 +34,6 @@ import io.olvid.engine.encoder.DecodingException; import io.olvid.engine.encoder.Encoded; import io.olvid.engine.datatypes.Constants; -import io.olvid.engine.metamanager.SolveChallengeDelegate; import io.olvid.engine.networkfetch.databases.ServerSession; import io.olvid.engine.networkfetch.datatypes.FetchManagerSession; import io.olvid.engine.networkfetch.datatypes.FetchManagerSessionFactory; @@ -44,14 +42,12 @@ class RequestChallengeOperation extends Operation { private final FetchManagerSessionFactory fetchManagerSessionFactory; private final SSLSocketFactory sslSocketFactory; private final Identity ownedIdentity; - private final SolveChallengeDelegate solveChallengeDelegate; - public RequestChallengeOperation(FetchManagerSessionFactory fetchManagerSessionFactory, SSLSocketFactory sslSocketFactory, Identity ownedIdentity, SolveChallengeDelegate solveChallengeDelegate) { + public RequestChallengeOperation(FetchManagerSessionFactory fetchManagerSessionFactory, SSLSocketFactory sslSocketFactory, Identity ownedIdentity) { super(ownedIdentity.computeUniqueUid(), null, null); this.fetchManagerSessionFactory = fetchManagerSessionFactory; this.sslSocketFactory = sslSocketFactory; this.ownedIdentity = ownedIdentity; - this.solveChallengeDelegate = solveChallengeDelegate; } public Identity getOwnedIdentity() { @@ -83,11 +79,6 @@ public void doExecute() { finished = true; return; } - UUID apiKey = solveChallengeDelegate.getApiKey(ownedIdentity); - if (apiKey == null) { - cancel(CreateServerSessionCompositeOperation.RFC_IDENTITY_NOT_FOUND); - return; - } PRNGService prng = Suite.getPRNGService(PRNG.PRNG_HMAC_SHA256); byte[] nonce = prng.bytes(Constants.SERVER_SESSION_NONCE_LENGTH); @@ -99,8 +90,7 @@ public void doExecute() { RequestChallengeServerMethod serverMethod = new RequestChallengeServerMethod( ownedIdentity, - nonce, - apiKey + nonce ); serverMethod.setSslSocketFactory(sslSocketFactory); @@ -144,15 +134,13 @@ class RequestChallengeServerMethod extends ServerMethod { private final String server; private final Identity identity; private final byte[] nonce; - private final UUID apiKey; private byte[] challenge = null; - public RequestChallengeServerMethod(Identity identity, byte[] nonce, UUID apiKey) { + public RequestChallengeServerMethod(Identity identity, byte[] nonce) { this.server = identity.getServer(); this.identity = identity; this.nonce = nonce; - this.apiKey = apiKey; } public byte[] getChallenge() { @@ -174,7 +162,6 @@ protected byte[] getDataToSend() { return Encoded.of(new Encoded[]{ Encoded.of(identity), Encoded.of(nonce), - Encoded.of(apiKey) }).getBytes(); } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/ServerQueryOperation.java b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/ServerQueryOperation.java index 10e76591..cf094dfa 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/ServerQueryOperation.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/ServerQueryOperation.java @@ -56,6 +56,9 @@ public class ServerQueryOperation extends Operation { public static final int RFC_INVALID_SERVER_SESSION = 3; public static final int RFC_IDENTITY_IS_INACTIVE = 4; public static final int RFC_USER_DATA_TOO_LARGE = 5; + public static final int RFC_DEVICE_DOES_NOT_EXIST = 6; + public static final int RFC_DEVICE_NOT_YET_REGISTERED = 7; + public static final int RFC_MALFORMED_URL = 8; private final FetchManagerSessionFactory fetchManagerSessionFactory; private final SSLSocketFactory sslSocketFactory; @@ -84,11 +87,6 @@ public Encoded getServerResponse() { return serverResponse; } - @Override - public void doCancel() { - // Nothings special to do on cancel - } - @Override public void doExecute() { boolean finished = false; @@ -155,11 +153,11 @@ public void doExecute() { AuthEnc authEnc = Suite.getAuthEnc(serverQuery.getType().getDataKey()); EncryptedBytes encryptedPhoto = authEnc.encrypt(serverQuery.getType().getDataKey(), buffer, prng); - serverMethod = new PutUserDataServerMethod(serverQuery.getType().getIdentity(), serverSessionToken, serverQuery.getType().getServerLabel(), encryptedPhoto); + serverMethod = new PutUserDataServerMethod(serverQuery.getType().getIdentity(), serverSessionToken, serverQuery.getType().getServerLabelOrDeviceUid(), encryptedPhoto); break; } case ServerQuery.Type.GET_USER_DATA_QUERY_ID: { - serverMethod = new GetUserDataServerMethod(serverQuery.getType().getIdentity(), serverQuery.getType().getServerLabel(), fetchManagerSession.engineBaseDirectory); + serverMethod = new GetUserDataServerMethod(serverQuery.getType().getIdentity(), serverQuery.getType().getServerLabelOrDeviceUid(), fetchManagerSession.engineBaseDirectory); break; } case ServerQuery.Type.CHECK_KEYCLOAK_REVOCATION_QUERY_ID: { @@ -172,31 +170,46 @@ public void doExecute() { cancel(RFC_INVALID_SERVER_SESSION); return; } - serverMethod = new CreateGroupBlobServerMethod(serverQuery.getOwnedIdentity(), serverSessionToken, serverQuery.getType().getServer(), serverQuery.getType().getServerLabel(), serverQuery.getType().getEncodedGroupAdminPublicKey(), serverQuery.getType().getEncryptedBlob()); + serverMethod = new CreateGroupBlobServerMethod(serverQuery.getOwnedIdentity(), serverSessionToken, serverQuery.getType().getServer(), serverQuery.getType().getServerLabelOrDeviceUid(), serverQuery.getType().getEncodedGroupAdminPublicKey(), serverQuery.getType().getEncryptedBlob()); break; } case ServerQuery.Type.GET_GROUP_BLOB_QUERY_ID: { - serverMethod = new GetGroupBlobServerMethod(serverQuery.getType().getServer(), serverQuery.getType().getServerLabel(), serverQuery.getType().getNonce()); + serverMethod = new GetGroupBlobServerMethod(serverQuery.getType().getServer(), serverQuery.getType().getServerLabelOrDeviceUid(), serverQuery.getType().getNonce()); break; } case ServerQuery.Type.LOCK_GROUP_BLOB_QUERY_ID: { - serverMethod = new LockGroupBlobServerMethod(serverQuery.getType().getServer(), serverQuery.getType().getServerLabel(), serverQuery.getType().getNonce(), serverQuery.getType().getQuerySignature()); + serverMethod = new LockGroupBlobServerMethod(serverQuery.getType().getServer(), serverQuery.getType().getServerLabelOrDeviceUid(), serverQuery.getType().getNonce(), serverQuery.getType().getQuerySignature()); break; } case ServerQuery.Type.UPDATE_GROUP_BLOB_QUERY_ID: { - serverMethod = new UpdateGroupBlobServerMethod(serverQuery.getType().getServer(), serverQuery.getType().getServerLabel(), serverQuery.getType().getNonce(), serverQuery.getType().getEncryptedBlob(), serverQuery.getType().getEncodedGroupAdminPublicKey(), serverQuery.getType().getQuerySignature()); + serverMethod = new UpdateGroupBlobServerMethod(serverQuery.getType().getServer(), serverQuery.getType().getServerLabelOrDeviceUid(), serverQuery.getType().getNonce(), serverQuery.getType().getEncryptedBlob(), serverQuery.getType().getEncodedGroupAdminPublicKey(), serverQuery.getType().getQuerySignature()); break; } case ServerQuery.Type.PUT_GROUP_LOG_QUERY_ID: { - serverMethod = new PutGroupLogServerMethod(serverQuery.getType().getServer(), serverQuery.getType().getServerLabel(), serverQuery.getType().getQuerySignature()); + serverMethod = new PutGroupLogServerMethod(serverQuery.getType().getServer(), serverQuery.getType().getServerLabelOrDeviceUid(), serverQuery.getType().getQuerySignature()); break; } case ServerQuery.Type.DELETE_GROUP_BLOB_QUERY_ID: { - serverMethod = new DeleteGroupBlobServerMethod(serverQuery.getType().getServer(), serverQuery.getType().getServerLabel(), serverQuery.getType().getQuerySignature()); + serverMethod = new DeleteGroupBlobServerMethod(serverQuery.getType().getServer(), serverQuery.getType().getServerLabelOrDeviceUid(), serverQuery.getType().getQuerySignature()); break; } case ServerQuery.Type.GET_KEYCLOAK_DATA_QUERY_ID: { - serverMethod = new GetKeycloakDataServerMethod(serverQuery.getType().getServer(), serverQuery.getType().getServerLabel(), fetchManagerSession.engineBaseDirectory); + serverMethod = new GetKeycloakDataServerMethod(serverQuery.getType().getServer(), serverQuery.getType().getServerLabelOrDeviceUid(), fetchManagerSession.engineBaseDirectory); + break; + } + case ServerQuery.Type.OWNED_DEVICE_DISCOVERY_QUERY_ID: { + serverMethod = new OwnedDeviceDiscoveryServerMethod(serverQuery.getType().getIdentity()); + break; + } + case ServerQuery.Type.DEVICE_MANAGEMENT_SET_NICKNAME_QUERY_ID: + case ServerQuery.Type.DEVICE_MANAGEMENT_DEACTIVATE_DEVICE_QUERY_ID: + case ServerQuery.Type.DEVICE_MANAGEMENT_SET_UNEXPIRING_DEVICE_QUERY_ID: { + byte[] serverSessionToken = ServerSession.getToken(fetchManagerSession, serverQuery.getOwnedIdentity()); + if (serverSessionToken == null) { + cancel(RFC_INVALID_SERVER_SESSION); + return; + } + serverMethod = new DeviceManagementServerMethod(serverQuery.getOwnedIdentity(), serverSessionToken, serverQuery.getType().getId(), serverQuery.getType().getServerLabelOrDeviceUid(), serverQuery.getType().getEncryptedBlob()); break; } default: @@ -223,12 +236,32 @@ public void doExecute() { case ServerMethod.PAYLOAD_TOO_LARGE: cancel(RFC_USER_DATA_TOO_LARGE); return; + case ServerMethod.DEVICE_IS_NOT_REGISTERED: { + // if the device is not registered: + // - cancel if this is a remote device + // - retry later if this is our current device for a set nickname request + if (serverQuery.getType().getId() == ServerQuery.Type.DEVICE_MANAGEMENT_SET_NICKNAME_QUERY_ID && serverQuery.getType().isCurrentDevice()) { + cancel(RFC_DEVICE_NOT_YET_REGISTERED); + } else { + cancel(RFC_DEVICE_DOES_NOT_EXIST); + } + return; + } + case ServerMethod.MALFORMED_URL: + cancel(RFC_MALFORMED_URL); + return; default: // check if the serverQuery has expired if (System.currentTimeMillis() > pendingServerQuery.getCreationTimestamp() + Constants.SERVER_QUERY_EXPIRATION_DELAY) { switch (serverQuery.getType().getId()) { case ServerQuery.Type.DEVICE_DISCOVERY_QUERY_ID: - serverResponse = Encoded.of(new Encoded[0]); // return an empty deviceUid list + serverResponse = Encoded.of(new Encoded[]{ + Encoded.of(Constants.BROADCAST_UID), + }); // return the broadcast deviceUid so we know it's not a real output + finished = true; + return; + case ServerQuery.Type.OWNED_DEVICE_DISCOVERY_QUERY_ID: + serverResponse = Encoded.of(new byte[0]); // return an empty byte array finished = true; return; case ServerQuery.Type.PUT_USER_DATA_QUERY_ID: @@ -273,6 +306,11 @@ public void doExecute() { e.printStackTrace(); } } + + @Override + public void doCancel() { + // Nothings special to do on cancel + } } abstract class ServerQueryServerMethod extends ServerMethod { @@ -340,6 +378,57 @@ public Encoded getServerResponse() { } } +class OwnedDeviceDiscoveryServerMethod extends ServerQueryServerMethod { + private static final String SERVER_METHOD_PATH = "/ownedDeviceDiscovery"; + + private final String server; + private final Identity identity; + + private Encoded serverResponse; + + public OwnedDeviceDiscoveryServerMethod(Identity identity) { + this.server = identity.getServer(); + this.identity = identity; + } + + @Override + protected String getServer() { + return server; + } + + @Override + protected String getServerMethod() { + return SERVER_METHOD_PATH; + } + + @Override + protected byte[] getDataToSend() { + return Encoded.of(new Encoded[]{ + Encoded.of(identity) + }).getBytes(); + } + + @Override + protected void parseReceivedData(Encoded[] receivedData) { + super.parseReceivedData(receivedData); + if (returnStatus == ServerMethod.OK) { + try { + // check that decoding works properly + receivedData[0].decodeEncryptedData(); + serverResponse = receivedData[0]; + } catch (DecodingException e) { + e.printStackTrace(); + returnStatus = ServerMethod.GENERAL_ERROR; + } + } + } + + @Override + public Encoded getServerResponse() { + return serverResponse; + } +} + class PutUserDataServerMethod extends ServerQueryServerMethod { private static final String SERVER_METHOD_PATH = "/putUserData"; @@ -950,3 +1039,138 @@ public Encoded getServerResponse() { return serverResponse; } } + +class DeviceManagementServerMethod extends ServerQueryServerMethod { + private static final String SERVER_METHOD_PATH = "/deviceManagement"; + + private final String server; + private final Identity identity; + private final byte[] token; + private final int queryType; + private final UID deviceUid; + private final EncryptedBytes encryptedDeviceName; + + private Encoded serverResponse; + + public DeviceManagementServerMethod(Identity identity, byte[] token, int queryType, UID deviceUid, EncryptedBytes encryptedDeviceName) { + this.server = identity.getServer(); + this.identity = identity; + this.token = token; + this.queryType = queryType; + this.deviceUid = deviceUid; + this.encryptedDeviceName = encryptedDeviceName; + } + + public UID getDeviceUid() { + return deviceUid; + } + + @Override + protected String getServer() { + return server; + } + + @Override + protected String getServerMethod() { + return SERVER_METHOD_PATH; + } + + @Override + protected byte[] getDataToSend() { + switch (queryType) { + case ServerQuery.Type.DEVICE_MANAGEMENT_SET_NICKNAME_QUERY_ID: { + return Encoded.of(new Encoded[]{ + Encoded.of(identity), + Encoded.of(token), + Encoded.of(new byte[]{(byte) 0x00}), + Encoded.of(deviceUid), + Encoded.of(encryptedDeviceName), + }).getBytes(); + } + case ServerQuery.Type.DEVICE_MANAGEMENT_DEACTIVATE_DEVICE_QUERY_ID: { + return Encoded.of(new Encoded[]{ + Encoded.of(identity), + Encoded.of(token), + Encoded.of(new byte[]{(byte) 0x01}), + Encoded.of(deviceUid), + }).getBytes(); + } + case ServerQuery.Type.DEVICE_MANAGEMENT_SET_UNEXPIRING_DEVICE_QUERY_ID: { + return Encoded.of(new Encoded[]{ + Encoded.of(identity), + Encoded.of(token), + Encoded.of(new byte[]{(byte) 0x02}), + Encoded.of(deviceUid), + }).getBytes(); + } + default: { + // invalid query type + return new byte[0]; + } + } + } + + @Override + protected void parseReceivedData(Encoded[] receivedData) { + super.parseReceivedData(receivedData); + if (returnStatus == ServerMethod.OK) { + if (receivedData.length == 0) { + // check that decoding works properly + serverResponse = null; + } else { + returnStatus = ServerMethod.GENERAL_ERROR; + } + } + } + + @Override + public Encoded getServerResponse() { + return serverResponse; + } +} + + +class RegisterApiKeyServerMethod extends ServerQueryServerMethod { + private static final String SERVER_METHOD_PATH = "/registerApiKey"; + + private final String server; + private final Identity ownedIdentity; + private final byte[] token; + private final String apiKeyString; + + public RegisterApiKeyServerMethod(Identity ownedIdentity, byte[] token, String apiKeyString) { + this.server = ownedIdentity.getServer(); + this.ownedIdentity = ownedIdentity; + this.token = token; + this.apiKeyString = apiKeyString; + } + + @Override + protected String getServer() { + return server; + } + + @Override + protected String getServerMethod() { + return SERVER_METHOD_PATH; + } + + @Override + protected byte[] getDataToSend() { + return Encoded.of(new Encoded[]{ + Encoded.of(ownedIdentity), + Encoded.of(token), + Encoded.of(apiKeyString), + }).getBytes(); + } + + @Override + protected void parseReceivedData(Encoded[] receivedData) { + super.parseReceivedData(receivedData); + } + + @Override + public Encoded getServerResponse() { + return null; + } +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/StandaloneServerQueryOperation.java b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/StandaloneServerQueryOperation.java new file mode 100644 index 00000000..71a8f5ab --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/StandaloneServerQueryOperation.java @@ -0,0 +1,105 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.networkfetch.operations; + +import io.olvid.engine.Logger; +import io.olvid.engine.datatypes.Operation; +import io.olvid.engine.datatypes.ServerMethod; +import io.olvid.engine.datatypes.containers.ServerQuery; +import io.olvid.engine.encoder.Encoded; + +public class StandaloneServerQueryOperation extends Operation { + public static final int RFC_NETWORK_ERROR = 1; + public static final int RFC_UNSUPPORTED_SERVER_QUERY_TYPE = 2; + public static final int RFC_INVALID_SERVER_SESSION = 3; + public static final int RFC_INVALID_API_KEY = 4; + + private final ServerQuery serverQuery; + private Encoded serverResponse; // will be set if the operation finishes normally + + public Encoded getServerResponse() { + return serverResponse; + } + + public StandaloneServerQueryOperation(ServerQuery serverQuery) { + this.serverQuery = serverQuery; + } + + @Override + public void doExecute() { + boolean finished = false; + try { + ServerQueryServerMethod serverMethod; + switch (serverQuery.getType().getId()) { + case ServerQuery.Type.OWNED_DEVICE_DISCOVERY_QUERY_ID: { + serverMethod = new OwnedDeviceDiscoveryServerMethod(serverQuery.getOwnedIdentity()); + break; + } + case ServerQuery.Type.REGISTER_API_KEY_QUERY_ID: { + serverMethod = new RegisterApiKeyServerMethod(serverQuery.getOwnedIdentity(), serverQuery.getType().getNonce(), serverQuery.getType().getDataUrl()); + break; + } + default: { + cancel(RFC_UNSUPPORTED_SERVER_QUERY_TYPE); + return; + } + } + + byte returnStatus = serverMethod.execute(true); + Logger.d("?? Server query return status (after parse): " + returnStatus); + + switch (returnStatus) { + case ServerMethod.OK: { + serverResponse = serverMethod.getServerResponse(); + finished = true; + return; + } + case ServerMethod.INVALID_SESSION: { + cancel(RFC_INVALID_SERVER_SESSION); + return; + } + case ServerMethod.INVALID_API_KEY: { + cancel(RFC_INVALID_API_KEY); + return; + } + default: { + cancel(RFC_NETWORK_ERROR); + return; + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (finished) { + setFinished(); + } else { + if (hasNoReasonForCancel()) { + cancel(null); + } + processCancel(); + } + } + } + + @Override + public void doCancel() { + // nothing to do here + } +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/VerifyReceiptOperation.java b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/VerifyReceiptOperation.java index 21ffd92a..61e51513 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/VerifyReceiptOperation.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/networkfetch/operations/VerifyReceiptOperation.java @@ -72,39 +72,40 @@ public void doExecute() { try (FetchManagerSession fetchManagerSession = fetchManagerSessionFactory.getSession()) { try { byte[] serverSessionToken = ServerSession.getToken(fetchManagerSession, ownedIdentity); - if (serverSessionToken != null) { - VerifyReceiptServerMethod serverMethod = new VerifyReceiptServerMethod( - ownedIdentity, - serverSessionToken, - storeToken - ); - serverMethod.setSslSocketFactory(sslSocketFactory); - - byte returnStatus = serverMethod.execute(fetchManagerSession.identityDelegate.isActiveOwnedIdentity(fetchManagerSession.session, ownedIdentity)); - - fetchManagerSession.session.startTransaction(); - switch (returnStatus) { - case ServerMethod.OK: { - HashMap userInfo = new HashMap<>(); - userInfo.put(DownloadNotifications.NOTIFICATION_VERIFY_RECEIPT_SUCCESS_OWNED_IDENTITY_KEY, ownedIdentity); - userInfo.put(DownloadNotifications.NOTIFICATION_VERIFY_RECEIPT_SUCCESS_STORE_TOKEN_KEY, storeToken); - fetchManagerSession.notificationPostingDelegate.postNotification(DownloadNotifications.NOTIFICATION_VERIFY_RECEIPT_SUCCESS, userInfo); - - fetchManagerSession.identityDelegate.updateApiKeyOfOwnedIdentity(fetchManagerSession.session, ownedIdentity, serverMethod.getApiKey()); - ServerSession.deleteForIdentity(fetchManagerSession, ownedIdentity); - fetchManagerSession.createServerSessionDelegate.createServerSession(ownedIdentity); - finished = true; - return; - } - case ServerMethod.INVALID_SESSION: { - ServerSession.deleteCurrentTokenIfEqualTo(fetchManagerSession, serverSessionToken, ownedIdentity); - fetchManagerSession.session.commit(); - cancel(RFC_INVALID_SERVER_SESSION); - break; - } - } + if (serverSessionToken == null) { + cancel(RFC_INVALID_SERVER_SESSION); + return; } + VerifyReceiptServerMethod serverMethod = new VerifyReceiptServerMethod( + ownedIdentity, + serverSessionToken, + storeToken + ); + serverMethod.setSslSocketFactory(sslSocketFactory); + + byte returnStatus = serverMethod.execute(fetchManagerSession.identityDelegate.isActiveOwnedIdentity(fetchManagerSession.session, ownedIdentity)); + + fetchManagerSession.session.startTransaction(); + switch (returnStatus) { + case ServerMethod.OK: { + HashMap userInfo = new HashMap<>(); + userInfo.put(DownloadNotifications.NOTIFICATION_VERIFY_RECEIPT_SUCCESS_OWNED_IDENTITY_KEY, ownedIdentity); + userInfo.put(DownloadNotifications.NOTIFICATION_VERIFY_RECEIPT_SUCCESS_STORE_TOKEN_KEY, storeToken); + fetchManagerSession.notificationPostingDelegate.postNotification(DownloadNotifications.NOTIFICATION_VERIFY_RECEIPT_SUCCESS, userInfo); + + ServerSession.deleteForIdentity(fetchManagerSession, ownedIdentity); + fetchManagerSession.createServerSessionDelegate.createServerSession(ownedIdentity); + finished = true; + return; + } + case ServerMethod.INVALID_SESSION: { + ServerSession.deleteCurrentTokenIfEqualTo(fetchManagerSession, serverSessionToken, ownedIdentity); + fetchManagerSession.session.commit(); + cancel(RFC_INVALID_SERVER_SESSION); + break; + } + } } catch (Exception e) { e.printStackTrace(); fetchManagerSession.session.rollback(); diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/ProtocolManager.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/ProtocolManager.java index 53011ec4..f929d145 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/ProtocolManager.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/ProtocolManager.java @@ -47,18 +47,23 @@ import io.olvid.engine.engine.types.JsonGroupDetailsWithVersionAndPhoto; import io.olvid.engine.engine.types.JsonIdentityDetailsWithVersionAndPhoto; import io.olvid.engine.engine.types.ObvCapability; +import io.olvid.engine.engine.types.ObvDeviceManagementRequest; import io.olvid.engine.engine.types.identities.ObvGroupV2; import io.olvid.engine.engine.types.identities.ObvKeycloakState; +import io.olvid.engine.engine.types.sync.ObvBackupAndSyncDelegate; +import io.olvid.engine.engine.types.sync.ObvSyncAtom; import io.olvid.engine.metamanager.ChannelDelegate; import io.olvid.engine.metamanager.CreateSessionDelegate; import io.olvid.engine.metamanager.EncryptionForIdentityDelegate; +import io.olvid.engine.metamanager.EngineOwnedIdentityCleanupDelegate; import io.olvid.engine.metamanager.FullRatchetProtocolStarterDelegate; import io.olvid.engine.metamanager.IdentityDelegate; -import io.olvid.engine.metamanager.NotificationListeningDelegate; import io.olvid.engine.metamanager.MetaManager; +import io.olvid.engine.metamanager.NotificationListeningDelegate; import io.olvid.engine.metamanager.NotificationPostingDelegate; import io.olvid.engine.metamanager.ObvManager; import io.olvid.engine.metamanager.ProtocolDelegate; +import io.olvid.engine.metamanager.PushNotificationDelegate; import io.olvid.engine.protocol.coordinators.ProtocolStepCoordinator; import io.olvid.engine.protocol.databases.ChannelCreationPingSignatureReceived; import io.olvid.engine.protocol.databases.ChannelCreationProtocolInstance; @@ -78,6 +83,8 @@ import io.olvid.engine.protocol.datatypes.ProtocolStarterDelegate; import io.olvid.engine.protocol.protocol_engine.ConcreteProtocol; import io.olvid.engine.protocol.protocols.ChannelCreationWithContactDeviceProtocol; +import io.olvid.engine.protocol.protocols.ChannelCreationWithOwnedDeviceProtocol; +import io.olvid.engine.protocol.protocols.ContactManagementProtocol; import io.olvid.engine.protocol.protocols.ContactMutualIntroductionProtocol; import io.olvid.engine.protocol.protocols.DeviceCapabilitiesDiscoveryProtocol; import io.olvid.engine.protocol.protocols.DeviceDiscoveryProtocol; @@ -90,9 +97,11 @@ import io.olvid.engine.protocol.protocols.IdentityDetailsPublicationProtocol; import io.olvid.engine.protocol.protocols.KeycloakBindingAndUnbindingProtocol; import io.olvid.engine.protocol.protocols.KeycloakContactAdditionProtocol; -import io.olvid.engine.protocol.protocols.ContactManagementProtocol; import io.olvid.engine.protocol.protocols.OneToOneContactInvitationProtocol; -import io.olvid.engine.protocol.protocols.OwnedIdentityDeletionWithContactNotificationProtocol; +import io.olvid.engine.protocol.protocols.OwnedDeviceDiscoveryProtocol; +import io.olvid.engine.protocol.protocols.OwnedDeviceManagementProtocol; +import io.olvid.engine.protocol.protocols.OwnedIdentityDeletionProtocol; +import io.olvid.engine.protocol.protocols.SynchronizationProtocol; import io.olvid.engine.protocol.protocols.TrustEstablishmentWithMutualScanProtocol; import io.olvid.engine.protocol.protocols.TrustEstablishmentWithSasProtocol; @@ -101,32 +110,43 @@ public class ProtocolManager implements ProtocolDelegate, ProtocolStarterDelegat private CreateSessionDelegate createSessionDelegate; private ChannelDelegate channelDelegate; private IdentityDelegate identityDelegate; + private ObvBackupAndSyncDelegate identityBackupAndSyncDelegate; private EncryptionForIdentityDelegate encryptionForIdentityDelegate; private NotificationPostingDelegate notificationPostingDelegate; + private EngineOwnedIdentityCleanupDelegate engineOwnedIdentityCleanupDelegate; + private PushNotificationDelegate pushNotificationDelegate; private final ProtocolStepCoordinator protocolStepCoordinator; private final String engineBaseDirectory; private final PRNGService prng; private final ObjectMapper jsonObjectMapper; - private final NewContactListener newContactListener; + private final NewDeviceListener newDeviceListener; private final ContactDeletedListener contactDeletedListener; private final ContactTrustLevelListener contactTrustLevelListener; + private final ObvBackupAndSyncDelegate appBackupAndSyncDelegate; + +// private final ScheduledExecutorService schedulerForPeriodicSync; - public ProtocolManager(MetaManager metaManager, String engineBaseDirectory, PRNGService prng, ObjectMapper jsonObjectMapper) { + public ProtocolManager(MetaManager metaManager, ObvBackupAndSyncDelegate appBackupAndSyncDelegate, String engineBaseDirectory, PRNGService prng, ObjectMapper jsonObjectMapper) { + this.appBackupAndSyncDelegate = appBackupAndSyncDelegate; this.engineBaseDirectory = engineBaseDirectory; this.prng = prng; this.jsonObjectMapper = jsonObjectMapper; this.protocolStepCoordinator = new ProtocolStepCoordinator(this, this.prng, this.jsonObjectMapper); - this.newContactListener = new NewContactListener(); + this.newDeviceListener = new NewDeviceListener(); this.contactDeletedListener = new ContactDeletedListener(); this.contactTrustLevelListener = new ContactTrustLevelListener(); +// this.schedulerForPeriodicSync = Executors.newScheduledThreadPool(1); metaManager.requestDelegate(this, CreateSessionDelegate.class); metaManager.requestDelegate(this, ChannelDelegate.class); metaManager.requestDelegate(this, EncryptionForIdentityDelegate.class); metaManager.requestDelegate(this, IdentityDelegate.class); + metaManager.requestDelegate(this, ObvBackupAndSyncDelegate.class); metaManager.requestDelegate(this, NotificationListeningDelegate.class); metaManager.requestDelegate(this, NotificationPostingDelegate.class); + metaManager.requestDelegate(this, EngineOwnedIdentityCleanupDelegate.class); + metaManager.requestDelegate(this, PushNotificationDelegate.class); metaManager.registerImplementedDelegates(this); } @@ -144,12 +164,55 @@ public void initialisationComplete() { protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message.generateChannelProtocolMessageToSend(), prng); } } + +// // trigger all SynchronizationProtocol instances to detect new changes and re-notify the app of current diffs +// for (ProtocolInstance protocolInstance : ProtocolInstance.getAllForProtocolId(protocolManagerSession, ConcreteProtocol.SYNCHRONIZATION_PROTOCOL_ID)) { +// CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(protocolInstance.getOwnedIdentity()), +// ConcreteProtocol.SYNCHRONIZATION_PROTOCOL_ID, +// protocolInstance.getUid(), +// false); +// ChannelMessageToSend message = new SynchronizationProtocol.TriggerSyncMessage(coreProtocolMessage, false).generateChannelProtocolMessageToSend(); +// protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message, prng); +// } +// +// // for all confirmed oblivious channels, initiate a SynchronizationProtocol in case one is not already running (message is ignored if protocol is already running) +// for (Identity ownedIdentity : identityDelegate.getOwnedIdentities(protocolManagerSession.session)) { +// UID currentDeviceUid = identityDelegate.getCurrentDeviceUidOfOwnedIdentity(protocolManagerSession.session, ownedIdentity); +// for (UID otherDeviceUid : channelDelegate.getConfirmedObliviousChannelDeviceUids(protocolManagerSession.session, ownedIdentity, ownedIdentity)) { +// CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(ownedIdentity), +// ConcreteProtocol.SYNCHRONIZATION_PROTOCOL_ID, +// SynchronizationProtocol.computeOngoingProtocolInstanceUid(ownedIdentity, currentDeviceUid, otherDeviceUid), +// false); +// ChannelMessageToSend message = new SynchronizationProtocol.InitiateSyncMessage(coreProtocolMessage, otherDeviceUid).generateChannelProtocolMessageToSend(); +// protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message, prng); +// } +// } + protocolManagerSession.session.commit(); } catch (Exception e) { e.printStackTrace(); } + +// schedulerForPeriodicSync.scheduleAtFixedRate(this::triggerOwnedDevicesSync, Constants.PERIODIC_OWNED_DEVICE_SYNC_INTERVAL, Constants.PERIODIC_OWNED_DEVICE_SYNC_INTERVAL, TimeUnit.MILLISECONDS); } +// private void triggerOwnedDevicesSync() { +// try (ProtocolManagerSession protocolManagerSession = getSession()) { +// for (ProtocolInstance protocolInstance : ProtocolInstance.getAllForProtocolId(protocolManagerSession, ConcreteProtocol.SYNCHRONIZATION_PROTOCOL_ID)) { +// CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(protocolInstance.getOwnedIdentity()), +// ConcreteProtocol.SYNCHRONIZATION_PROTOCOL_ID, +// protocolInstance.getUid(), +// false); +// ChannelMessageToSend message = new SynchronizationProtocol.TriggerSyncMessage(coreProtocolMessage, false).generateChannelProtocolMessageToSend(); +// protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message, prng); +// } +// +// protocolManagerSession.session.commit(); +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } + // region SetDelegate public void setDelegate(CreateSessionDelegate createSessionDelegate) { @@ -193,13 +256,17 @@ public void setDelegate(ChannelDelegate channelDelegate) { public void setDelegate(IdentityDelegate identityDelegate) { this.identityDelegate = identityDelegate; } + public void setDelegate(ObvBackupAndSyncDelegate identityBackupAndSyncDelegate) { + this.identityBackupAndSyncDelegate = identityBackupAndSyncDelegate; + } public void setDelegate(EncryptionForIdentityDelegate encryptionForIdentityDelegate) { this.encryptionForIdentityDelegate = encryptionForIdentityDelegate; } public void setDelegate(NotificationListeningDelegate notificationListeningDelegate) { - notificationListeningDelegate.addListener(IdentityNotifications.NOTIFICATION_NEW_CONTACT_DEVICE, newContactListener); + notificationListeningDelegate.addListener(IdentityNotifications.NOTIFICATION_NEW_CONTACT_DEVICE, newDeviceListener); + notificationListeningDelegate.addListener(IdentityNotifications.NOTIFICATION_NEW_OWNED_DEVICE, newDeviceListener); notificationListeningDelegate.addListener(IdentityNotifications.NOTIFICATION_CONTACT_IDENTITY_DELETED, contactDeletedListener); notificationListeningDelegate.addListener(IdentityNotifications.NOTIFICATION_CONTACT_ONE_TO_ONE_CHANGED, contactTrustLevelListener); } @@ -208,22 +275,47 @@ public void setDelegate(NotificationPostingDelegate notificationPostingDelegate) this.notificationPostingDelegate = notificationPostingDelegate; } + public void setDelegate(EngineOwnedIdentityCleanupDelegate engineOwnedIdentityCleanupDelegate) { + this.engineOwnedIdentityCleanupDelegate = engineOwnedIdentityCleanupDelegate; + } + + public void setDelegate(PushNotificationDelegate pushNotificationDelegate) { + this.pushNotificationDelegate = pushNotificationDelegate; + } + // endregion - class NewContactListener implements NotificationListener { + class NewDeviceListener implements NotificationListener { @Override public void callback(String notificationName, HashMap userInfo) { switch (notificationName) { - case IdentityNotifications.NOTIFICATION_NEW_CONTACT_DEVICE: + case IdentityNotifications.NOTIFICATION_NEW_CONTACT_DEVICE: { try { UID contactDeviceUid = (UID) userInfo.get(IdentityNotifications.NOTIFICATION_NEW_CONTACT_DEVICE_CONTACT_DEVICE_UID_KEY); Identity contactIdentity = (Identity) userInfo.get(IdentityNotifications.NOTIFICATION_NEW_CONTACT_DEVICE_CONTACT_IDENTITY_KEY); Identity ownedIdentity = (Identity) userInfo.get(IdentityNotifications.NOTIFICATION_NEW_CONTACT_DEVICE_OWNED_IDENTITY_KEY); - startChannelCreationWithContactDeviceProtocol(ownedIdentity, contactIdentity, contactDeviceUid); + Boolean channelCreationAlreadyInProgress = (Boolean) userInfo.get(IdentityNotifications.NOTIFICATION_NEW_CONTACT_DEVICE_CHANNEL_CREATION_ALREADY_IN_PROGRESS_KEY); + if (channelCreationAlreadyInProgress == null || !channelCreationAlreadyInProgress) { + startChannelCreationWithContactDeviceProtocol(ownedIdentity, contactIdentity, contactDeviceUid); + } } catch (Exception e) { e.printStackTrace(); } break; + } + case IdentityNotifications.NOTIFICATION_NEW_OWNED_DEVICE: { + try { + UID ownedDeviceUid = (UID) userInfo.get(IdentityNotifications.NOTIFICATION_NEW_OWNED_DEVICE_DEVICE_UID_KEY); + Identity ownedIdentity = (Identity) userInfo.get(IdentityNotifications.NOTIFICATION_NEW_OWNED_DEVICE_OWNED_IDENTITY_KEY); + Boolean channelCreationAlreadyInProgress = (Boolean) userInfo.get(IdentityNotifications.NOTIFICATION_NEW_OWNED_DEVICE_CHANNEL_CREATION_ALREADY_IN_PROGRESS_KEY); + if (channelCreationAlreadyInProgress == null || !channelCreationAlreadyInProgress) { + startChannelCreationWithOwnedDeviceProtocol(ownedIdentity, ownedDeviceUid); + } + } catch (Exception e) { + e.printStackTrace(); + } + break; + } } } } @@ -245,7 +337,7 @@ public void callback(String notificationName, HashMap userInfo) abortProtocol(protocolManagerSession.session, channelCreationProtocolInstance.getProtocolInstanceUid(), ownedIdentity); } } - // TODO: delete any other protocol related to this contact + // To improve: delete any other protocol related to this contact } catch (Exception e) { e.printStackTrace(); } @@ -281,11 +373,11 @@ public void callback(String notificationName, HashMap userInfo) } } - public void deleteOwnedIdentity(Session session, Identity ownedIdentity) throws SQLException { + public void deleteOwnedIdentity(Session session, Identity ownedIdentity, UID excludedProtocolInstanceUid) throws SQLException { // delete ReceivedMessage ReceivedMessage.deleteAllForOwnedIdentity(wrapSession(session), ownedIdentity); // delete ProtocolInstance - ProtocolInstance.deleteAllForOwnedIdentity(wrapSession(session), ownedIdentity); + ProtocolInstance.deleteAllForOwnedIdentity(wrapSession(session), ownedIdentity, excludedProtocolInstanceUid); // delete TrustEstablishmentCommitmentReceived TrustEstablishmentCommitmentReceived.deleteAllForOwnedIdentity(wrapSession(session), ownedIdentity); // delete MutualScanNonceReceived @@ -300,6 +392,8 @@ public void deleteOwnedIdentity(Session session, Identity ownedIdentity) throws ChannelCreationPingSignatureReceived.deleteAllForOwnedIdentity(wrapSession(session), ownedIdentity); // delete WaitingForTrustLevelIncreaseProtocolInstance WaitingForOneToOneContactProtocolInstance.deleteAllForOwnedIdentity(wrapSession(session), ownedIdentity); + // delete IdentityDeletionSignatureReceived + IdentityDeletionSignatureReceived.deleteAllForOwnedIdentity(wrapSession(session), ownedIdentity); } // region Implement ProtocolDelegate @@ -307,7 +401,7 @@ public void deleteOwnedIdentity(Session session, Identity ownedIdentity) throws @Override public void abortProtocol(Session session, UID protocolInstanceUid, Identity ownedIdentity) throws Exception { - // TODO: execute this on the protocol step execution thread + // To improve: execute this on the protocol step execution thread // move this to the ProtocolReceivedMessageProcessorDelegate API instead (in the ProtocolStepCoordinator) // do something "protocol specific" to notify other devices that protocol was aborted when necessary. ProtocolManagerSession protocolManagerSession = wrapSession(session); @@ -319,7 +413,9 @@ public void abortProtocol(Session session, UID protocolInstanceUid, Identity own // Delete the associated ProtocolInstance ProtocolInstance protocolInstance = ProtocolInstance.get(protocolManagerSession, protocolInstanceUid, ownedIdentity); - protocolInstance.delete(); + if (protocolInstance != null) { + protocolInstance.delete(); + } // Delete all remaining ReceivedMessage for this protocol for (ReceivedMessage receivedMessage: ReceivedMessage.getAll(protocolManagerSession, protocolInstanceUid, ownedIdentity)) { @@ -391,11 +487,11 @@ public ProtocolManagerSession getSession() throws SQLException { if (createSessionDelegate == null) { throw new SQLException("No CreateSessionDelegate was set in ChannelManager."); } - return new ProtocolManagerSession(createSessionDelegate.getSession(), channelDelegate, identityDelegate, encryptionForIdentityDelegate, protocolStepCoordinator, this, this, notificationPostingDelegate, engineBaseDirectory); + return new ProtocolManagerSession(createSessionDelegate.getSession(), channelDelegate, identityDelegate, encryptionForIdentityDelegate, protocolStepCoordinator, this, this, notificationPostingDelegate, engineOwnedIdentityCleanupDelegate, pushNotificationDelegate, engineBaseDirectory, identityBackupAndSyncDelegate, appBackupAndSyncDelegate); } private ProtocolManagerSession wrapSession(Session session) { - return new ProtocolManagerSession(session, channelDelegate, identityDelegate, encryptionForIdentityDelegate, protocolStepCoordinator, this, this, notificationPostingDelegate, engineBaseDirectory); + return new ProtocolManagerSession(session, channelDelegate, identityDelegate, encryptionForIdentityDelegate, protocolStepCoordinator, this, this, notificationPostingDelegate, engineOwnedIdentityCleanupDelegate, pushNotificationDelegate, engineBaseDirectory, identityBackupAndSyncDelegate, appBackupAndSyncDelegate); } // endregion @@ -437,6 +533,44 @@ public void startDeviceDiscoveryProtocolWithinTransaction(Session session, Ident protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message, prng); } + @Override + public void startOwnedDeviceDiscoveryProtocol(Identity ownedIdentity) throws Exception { + try (ProtocolManagerSession protocolManagerSession = getSession()) { + UID protocolInstanceUid = new UID(prng); + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(ownedIdentity), + ConcreteProtocol.OWNED_DEVICE_DISCOVERY_PROTOCOL_ID, + protocolInstanceUid, + false); + ChannelMessageToSend message = new OwnedDeviceDiscoveryProtocol.InitialMessage(coreProtocolMessage).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message, prng); + protocolManagerSession.session.commit(); + } + } + + @Override + public void startOwnedDeviceDiscoveryProtocolWithinTransaction(Session session, Identity ownedIdentity) throws Exception { + ProtocolManagerSession protocolManagerSession = wrapSession(session); + UID protocolInstanceUid = new UID(prng); + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(ownedIdentity), + ConcreteProtocol.OWNED_DEVICE_DISCOVERY_PROTOCOL_ID, + protocolInstanceUid, + false); + ChannelMessageToSend message = new OwnedDeviceDiscoveryProtocol.InitialMessage(coreProtocolMessage).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(session, message, prng); + } + + @Override + public void startChannelCreationProtocolWithOwnedDevice(Session session, Identity ownedIdentity, UID ownedDeviceUid) throws Exception { + ProtocolManagerSession protocolManagerSession = wrapSession(session); + UID protocolInstanceUid = new UID(prng); + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(ownedIdentity), + ConcreteProtocol.CHANNEL_CREATION_WITH_OWNED_DEVICE_PROTOCOL_ID, + protocolInstanceUid, + false); + ChannelMessageToSend message = new ChannelCreationWithOwnedDeviceProtocol.InitialMessage(coreProtocolMessage, ownedDeviceUid).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(session, message, prng); + } + @Override public void startDownloadIdentityPhotoProtocolWithinTransaction(Session session, Identity ownedIdentity, Identity contactIdentity, JsonIdentityDetailsWithVersionAndPhoto jsonIdentityDetailsWithVersionAndPhoto) throws Exception { if (ownedIdentity == null || contactIdentity == null || jsonIdentityDetailsWithVersionAndPhoto == null) { @@ -570,8 +704,7 @@ public void startMutualScanTrustEstablishmentProtocol(Identity ownedIdentity, Id } - @Override - public void startChannelCreationWithContactDeviceProtocol(Identity ownedIdentity, Identity contactIdentity, UID contactDeviceUid) throws Exception { + private void startChannelCreationWithContactDeviceProtocol(Identity ownedIdentity, Identity contactIdentity, UID contactDeviceUid) throws Exception { if (contactIdentity.equals(ownedIdentity)) { Logger.w("Cannot start a protocol with contactIdentity == ownedIdentity"); return; @@ -588,6 +721,19 @@ public void startChannelCreationWithContactDeviceProtocol(Identity ownedIdentity } } + public void startChannelCreationWithOwnedDeviceProtocol(Identity ownedIdentity, UID ownedDeviceUid) throws Exception { + try (ProtocolManagerSession protocolManagerSession = getSession()) { + UID protocolInstanceUid = new UID(prng); + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(ownedIdentity), + ConcreteProtocol.CHANNEL_CREATION_WITH_OWNED_DEVICE_PROTOCOL_ID, + protocolInstanceUid, + false); + ChannelMessageToSend message = new ChannelCreationWithOwnedDeviceProtocol.InitialMessage(coreProtocolMessage, ownedDeviceUid).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message, prng); + protocolManagerSession.session.commit(); + } + } + @Override public void startContactMutualIntroductionProtocol(Identity ownedIdentity, Identity contactIdentityA, Identity[] contactIdentities) throws Exception { if (contactIdentityA.equals(ownedIdentity)) { @@ -641,7 +787,7 @@ public void startGroupCreationProtocol(Identity ownedIdentity, String serialized } - public void startGroupV2CreationProtocol(Identity ownedIdentity, String serializedGroupDetails, String absolutePhotoUrl, HashSet ownPermissions, HashSet otherGroupMembers) throws Exception { + public void startGroupV2CreationProtocol(Identity ownedIdentity, String serializedGroupDetails, String absolutePhotoUrl, HashSet ownPermissions, HashSet otherGroupMembers, String serializedGroupType) throws Exception { if (serializedGroupDetails == null || ownedIdentity == null || ownPermissions == null || otherGroupMembers == null) { throw new Exception(); } @@ -664,7 +810,7 @@ public void startGroupV2CreationProtocol(Identity ownedIdentity, String serializ protocolInstanceUid, false); - ChannelMessageToSend message = new GroupsV2Protocol.GroupCreationInitialMessage(coreProtocolMessage, ownPermissions, otherGroupMembers, serializedGroupDetails, absolutePhotoUrl).generateChannelProtocolMessageToSend(); + ChannelMessageToSend message = new GroupsV2Protocol.GroupCreationInitialMessage(coreProtocolMessage, ownPermissions, otherGroupMembers, serializedGroupDetails, absolutePhotoUrl, serializedGroupType).generateChannelProtocolMessageToSend(); protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message, prng); protocolManagerSession.session.commit(); } @@ -692,7 +838,7 @@ public void initiateGroupV2Update(Identity ownedIdentity, GroupV2.Identifier gro @Override public void initiateGroupV2Leave(Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws Exception { - if (ownedIdentity == null || groupIdentifier == null) { + if (ownedIdentity == null || groupIdentifier == null || groupIdentifier.category == GroupV2.Identifier.CATEGORY_KEYCLOAK) { throw new Exception(); } @@ -712,7 +858,7 @@ public void initiateGroupV2Leave(Identity ownedIdentity, GroupV2.Identifier grou @Override public void initiateGroupV2Disband(Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws Exception { - if (ownedIdentity == null || groupIdentifier == null) { + if (ownedIdentity == null || groupIdentifier == null || groupIdentifier.category == GroupV2.Identifier.CATEGORY_KEYCLOAK) { throw new Exception(); } @@ -782,6 +928,20 @@ public void createOrUpdateKeycloakGroupV2(Session session, Identity ownedIdentit } + @Override + public void processDeviceManagementRequest(Session session, Identity ownedIdentity, ObvDeviceManagementRequest deviceManagementRequest) throws Exception { + if (session == null || ownedIdentity == null || deviceManagementRequest == null) { + throw new Exception(); + } + + UID protocolInstanceUid = new UID(prng); + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(ownedIdentity), + ConcreteProtocol.OWNED_DEVICE_MANAGEMENT_PROTOCOL_ID, + protocolInstanceUid, + false); + ChannelMessageToSend message = new OwnedDeviceManagementProtocol.InitialMessage(coreProtocolMessage, deviceManagementRequest).generateChannelProtocolMessageToSend(); + channelDelegate.post(session, message, prng); + } @Override public void startIdentityDetailsPublicationProtocol(Session session, Identity ownedIdentity, int version) throws Exception { @@ -1217,7 +1377,7 @@ public void startProtocolForUnbindingOwnedIdentityFromKeycloak(Identity ownedIde } @Override - public void deleteOwnedIdentityAndNotifyContacts(Session session, Identity ownedIdentity) throws Exception { + public void startOwnedIdentityDeletionProtocol(Session session, Identity ownedIdentity, boolean deleteEverywhere) throws Exception { if (session == null || ownedIdentity == null) { throw new Exception(); } @@ -1225,21 +1385,56 @@ public void deleteOwnedIdentityAndNotifyContacts(Session session, Identity owned UID protocolInstanceUid = new UID(prng); CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(ownedIdentity), - ConcreteProtocol.OWNED_IDENTITY_DELETION_WITH_CONTACT_NOTIFICATION_PROTOCOL_ID, + ConcreteProtocol.OWNED_IDENTITY_DELETION_PROTOCOL_ID, + protocolInstanceUid, + false); + + ChannelMessageToSend message = new OwnedIdentityDeletionProtocol.InitialMessage(coreProtocolMessage, deleteEverywhere).generateChannelProtocolMessageToSend(); + channelDelegate.post(session, message, prng); + } + + + @Override + public void initiateSingleItemSync(Session session, Identity ownedIdentity, ObvSyncAtom obvSyncAtom) throws Exception { + if (session == null || ownedIdentity == null || obvSyncAtom == null) { + throw new Exception(); + } + + UID protocolInstanceUid = new UID(prng); + + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(ownedIdentity), + ConcreteProtocol.SYNCHRONIZATION_PROTOCOL_ID, protocolInstanceUid, false); - ChannelMessageToSend message = new OwnedIdentityDeletionWithContactNotificationProtocol.InitialMessage(coreProtocolMessage).generateChannelProtocolMessageToSend(); + ChannelMessageToSend message = new SynchronizationProtocol.InitiateSingleItemSyncMessage(coreProtocolMessage, obvSyncAtom).generateChannelProtocolMessageToSend(); channelDelegate.post(session, message, prng); } +// @Override +// public void triggerOwnedDevicesSync(Session session, Identity ownedIdentity) { +// try { +// ProtocolManagerSession protocolManagerSession = wrapSession(session); +// for (ProtocolInstance protocolInstance : ProtocolInstance.getAllForOwnedIdentityProtocolId(protocolManagerSession, ownedIdentity, ConcreteProtocol.SYNCHRONIZATION_PROTOCOL_ID)) { +// CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(ownedIdentity), +// ConcreteProtocol.SYNCHRONIZATION_PROTOCOL_ID, +// protocolInstance.getUid(), +// false); +// ChannelMessageToSend message = new SynchronizationProtocol.TriggerSyncMessage(coreProtocolMessage, true).generateChannelProtocolMessageToSend(); +// protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message, prng); +// } +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } + // endregion // region Implement FullRatchetProtocolStarterDelegate @Override public void startFullRatchetProtocolForObliviousChannel(UID currentDeviceUid, UID remoteDeviceUid, Identity remoteIdentity) throws Exception { try (ProtocolManagerSession protocolManagerSession = getSession()) { - Identity ownedIdentity = identityDelegate.getOwnedIdentityForDeviceUid(protocolManagerSession.session, currentDeviceUid); + Identity ownedIdentity = identityDelegate.getOwnedIdentityForCurrentDeviceUid(protocolManagerSession.session, currentDeviceUid); if (ownedIdentity != null) { UID protocolInstanceUid = FullRatchetProtocol.computeProtocolUid(ownedIdentity, remoteIdentity, currentDeviceUid, remoteDeviceUid); diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/coordinators/ProtocolStepCoordinator.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/coordinators/ProtocolStepCoordinator.java index a4afb73e..3acd1a37 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/coordinators/ProtocolStepCoordinator.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/coordinators/ProtocolStepCoordinator.java @@ -68,7 +68,7 @@ private void queueNewProtocolOperation(UID receivedMessageUid) { public void initialQueueing() { try (ProtocolManagerSession protocolManagerSession = protocolManagerSessionFactory.getSession()) { - // TODO: also cleanup protocol instances: implement a clean abort in each protocol, and call it when the protocol is stalled + // To improve: also cleanup protocol instances: implement a clean abort in each protocol, and call it when the protocol is stalled ReceivedMessage.deleteExpiredMessagesWithNoProtocol(protocolManagerSession); ReceivedMessage[] receivedMessages = ReceivedMessage.getAll(protocolManagerSession); @@ -123,12 +123,12 @@ public void onCancelCallback(Operation operation) { case ProtocolOperation.RFC_THE_STEP_TO_EXECUTE_FAILED: { // check how many times this step has failed before UID messageUid = ((ProtocolOperation) operation).getReceivedMessageUid(); - Integer failedAttemps = stepFailedAttemptCount.get(messageUid); - if (failedAttemps == null) { - failedAttemps = 0; + Integer failedAttempts = stepFailedAttemptCount.get(messageUid); + if (failedAttempts == null) { + failedAttempts = 0; } - failedAttemps++; - if (failedAttemps >= 5) { + failedAttempts++; + if (failedAttempts >= 5) { // the step failed 5 times --> we can delete it try (ProtocolManagerSession protocolManagerSession = protocolManagerSessionFactory.getSession()) { ReceivedMessage message = ReceivedMessage.get(protocolManagerSession, ((ProtocolOperation) operation).getReceivedMessageUid()); @@ -139,7 +139,7 @@ public void onCancelCallback(Operation operation) { } } else { // retry to execute the step - stepFailedAttemptCount.put(messageUid, failedAttemps); + stepFailedAttemptCount.put(messageUid, failedAttempts); queueNewProtocolOperation(messageUid); } break; diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/databases/ChannelCreationProtocolInstance.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/databases/ChannelCreationProtocolInstance.java index ef6c7df9..3e39715d 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/databases/ChannelCreationProtocolInstance.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/databases/ChannelCreationProtocolInstance.java @@ -59,7 +59,8 @@ public static ChannelCreationProtocolInstance create(ProtocolManagerSession prot return null; } ProtocolInstance protocolInstance = ProtocolInstance.get(protocolManagerSession, protocolInstanceUid, ownedIdentity); - if ((protocolInstance == null) || (protocolInstance.getProtocolId() != ConcreteProtocol.CHANNEL_CREATION_WITH_CONTACT_DEVICE_PROTOCOL_ID)) { + if ((protocolInstance == null) + || (protocolInstance.getProtocolId() != ConcreteProtocol.CHANNEL_CREATION_WITH_CONTACT_DEVICE_PROTOCOL_ID && protocolInstance.getProtocolId() != ConcreteProtocol.CHANNEL_CREATION_WITH_OWNED_DEVICE_PROTOCOL_ID)) { return null; } try { diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/databases/ProtocolInstance.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/databases/ProtocolInstance.java index 72c187be..55285a26 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/databases/ProtocolInstance.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/databases/ProtocolInstance.java @@ -24,6 +24,9 @@ import java.sql.SQLException; import java.sql.Statement; import java.sql.Types; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import io.olvid.engine.Logger; import io.olvid.engine.datatypes.Identity; @@ -41,7 +44,7 @@ public class ProtocolInstance implements ObvDatabase { private final ProtocolManagerSession protocolManagerSession; - // TODO: add an expiration timestamp, updated each time a new state is written + // To improve: add an expiration timestamp, updated each time a new state is written // --> this timestamp should depend on the protocol type (infinite for group management) private UID uid; @@ -101,8 +104,8 @@ public void updateCurrentState(ConcreteProtocolState newState) throws SQLExcepti - public static ProtocolInstance create(ProtocolManagerSession protocolManagerSession, UID protocolInstanceUid, Identity ownedIdentity, int protocolId, ConcreteProtocolState initialState) { - if ((protocolInstanceUid == null) || (ownedIdentity == null) || (initialState == null)) { + public static ProtocolInstance create(ProtocolManagerSession protocolManagerSession, UID protocolInstanceUid, Identity ownedIdentity, int protocolId, ConcreteProtocolState protocolState) { + if ((protocolInstanceUid == null) || (ownedIdentity == null) || (protocolState == null)) { return null; } try { @@ -111,8 +114,8 @@ public static ProtocolInstance create(ProtocolManagerSession protocolManagerSess protocolInstanceUid, ownedIdentity, protocolId, - initialState.id, - initialState.encode() + protocolState.id, + protocolState.encode() ); protocolInstance.insert(); return protocolInstance; @@ -228,11 +231,53 @@ public static ProtocolInstance get(ProtocolManagerSession protocolManagerSession } } + public static List getAllForProtocolId(ProtocolManagerSession protocolManagerSession, int protocolId) { + try (PreparedStatement statement = protocolManagerSession.session.prepareStatement("SELECT * FROM " + TABLE_NAME + " WHERE " + PROTOCOL_ID + " = ?;")) { + statement.setInt(1, protocolId); + try (ResultSet res = statement.executeQuery()) { + List list = new ArrayList<>(); + while (res.next()) { + list.add(new ProtocolInstance(protocolManagerSession, res)); + } + return list; + } + } catch (SQLException e) { + return Collections.emptyList(); + } + } - public static void deleteAllForOwnedIdentity(ProtocolManagerSession protocolManagerSession, Identity ownedIdentity) throws SQLException { - try (PreparedStatement statement = protocolManagerSession.session.prepareStatement("DELETE FROM " + TABLE_NAME + " WHERE " + OWNED_IDENTITY + " = ?;")) { - statement.setBytes(1, ownedIdentity.getBytes()); - statement.executeUpdate(); + public static List getAllForOwnedIdentityProtocolId(ProtocolManagerSession protocolManagerSession, Identity ownedIdentity, int protocolId) { + try (PreparedStatement statement = protocolManagerSession.session.prepareStatement("SELECT * FROM " + TABLE_NAME + + " WHERE " + PROTOCOL_ID + " = ? " + + " AND " + OWNED_IDENTITY + " = ?;")) { + statement.setInt(1, protocolId); + statement.setBytes(2, ownedIdentity.getBytes()); + try (ResultSet res = statement.executeQuery()) { + List list = new ArrayList<>(); + while (res.next()) { + list.add(new ProtocolInstance(protocolManagerSession, res)); + } + return list; + } + } catch (SQLException e) { + return Collections.emptyList(); + } + } + + public static void deleteAllForOwnedIdentity(ProtocolManagerSession protocolManagerSession, Identity ownedIdentity, UID excludedProtocolInstanceUid) throws SQLException { + if (excludedProtocolInstanceUid == null) { + try (PreparedStatement statement = protocolManagerSession.session.prepareStatement("DELETE FROM " + TABLE_NAME + " WHERE " + OWNED_IDENTITY + " = ?;")) { + statement.setBytes(1, ownedIdentity.getBytes()); + statement.executeUpdate(); + } + } else { + try (PreparedStatement statement = protocolManagerSession.session.prepareStatement("DELETE FROM " + TABLE_NAME + + " WHERE " + OWNED_IDENTITY + " = ?" + + " AND " + UID_ + " != ?;")) { + statement.setBytes(1, ownedIdentity.getBytes()); + statement.setBytes(2, excludedProtocolInstanceUid.getBytes()); + statement.executeUpdate(); + } } } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/datatypes/ProtocolManagerSession.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/datatypes/ProtocolManagerSession.java index e966527c..b2afec0d 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/datatypes/ProtocolManagerSession.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/datatypes/ProtocolManagerSession.java @@ -23,11 +23,14 @@ import java.sql.SQLException; import io.olvid.engine.datatypes.Session; +import io.olvid.engine.engine.types.sync.ObvBackupAndSyncDelegate; import io.olvid.engine.metamanager.ChannelDelegate; import io.olvid.engine.metamanager.EncryptionForIdentityDelegate; +import io.olvid.engine.metamanager.EngineOwnedIdentityCleanupDelegate; import io.olvid.engine.metamanager.IdentityDelegate; import io.olvid.engine.metamanager.NotificationPostingDelegate; import io.olvid.engine.metamanager.ProtocolDelegate; +import io.olvid.engine.metamanager.PushNotificationDelegate; public class ProtocolManagerSession implements AutoCloseable { public final Session session; @@ -38,9 +41,13 @@ public class ProtocolManagerSession implements AutoCloseable { public final ProtocolStarterDelegate protocolStarterDelegate; public final ProtocolDelegate protocolDelegate; public final NotificationPostingDelegate notificationPostingDelegate; + public final EngineOwnedIdentityCleanupDelegate engineOwnedIdentityCleanupDelegate; + public final PushNotificationDelegate pushNotificationDelegate; public final String engineBaseDirectory; + public final ObvBackupAndSyncDelegate identityBackupAndSyncDelegate; + public final ObvBackupAndSyncDelegate appBackupAndSyncDelegate; - public ProtocolManagerSession(Session session, ChannelDelegate channelDelegate, IdentityDelegate identityDelegate, EncryptionForIdentityDelegate encryptionForIdentityDelegate, ProtocolReceivedMessageProcessorDelegate protocolReceivedMessageProcessorDelegate, ProtocolStarterDelegate protocolStarterDelegate, ProtocolDelegate protocolDelegate, NotificationPostingDelegate notificationPostingDelegate, String engineBaseDirectory) { + public ProtocolManagerSession(Session session, ChannelDelegate channelDelegate, IdentityDelegate identityDelegate, EncryptionForIdentityDelegate encryptionForIdentityDelegate, ProtocolReceivedMessageProcessorDelegate protocolReceivedMessageProcessorDelegate, ProtocolStarterDelegate protocolStarterDelegate, ProtocolDelegate protocolDelegate, NotificationPostingDelegate notificationPostingDelegate, EngineOwnedIdentityCleanupDelegate engineOwnedIdentityCleanupDelegate, PushNotificationDelegate pushNotificationDelegate, String engineBaseDirectory, ObvBackupAndSyncDelegate identityBackupAndSyncDelegate, ObvBackupAndSyncDelegate appBackupAndSyncDelegate) { this.session = session; this.channelDelegate = channelDelegate; this.identityDelegate = identityDelegate; @@ -49,7 +56,11 @@ public ProtocolManagerSession(Session session, ChannelDelegate channelDelegate, this.protocolStarterDelegate = protocolStarterDelegate; this.protocolDelegate = protocolDelegate; this.notificationPostingDelegate = notificationPostingDelegate; + this.engineOwnedIdentityCleanupDelegate = engineOwnedIdentityCleanupDelegate; + this.pushNotificationDelegate = pushNotificationDelegate; this.engineBaseDirectory = engineBaseDirectory; + this.identityBackupAndSyncDelegate = identityBackupAndSyncDelegate; + this.appBackupAndSyncDelegate = appBackupAndSyncDelegate; } @Override diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/datatypes/ProtocolStarterDelegate.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/datatypes/ProtocolStarterDelegate.java index 3bca894d..4aaee452 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/datatypes/ProtocolStarterDelegate.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/datatypes/ProtocolStarterDelegate.java @@ -31,24 +31,29 @@ import io.olvid.engine.engine.types.JsonGroupDetailsWithVersionAndPhoto; import io.olvid.engine.engine.types.JsonIdentityDetailsWithVersionAndPhoto; import io.olvid.engine.engine.types.ObvCapability; +import io.olvid.engine.engine.types.ObvDeviceManagementRequest; +import io.olvid.engine.engine.types.sync.ObvSyncAtom; import io.olvid.engine.engine.types.identities.ObvGroupV2; import io.olvid.engine.engine.types.identities.ObvKeycloakState; public interface ProtocolStarterDelegate { void startDeviceDiscoveryProtocol(Identity ownedIdentity, Identity contactIdentity) throws Exception; void startDeviceDiscoveryProtocolWithinTransaction(Session session, Identity ownedIdentity, Identity contactIdentity) throws Exception; + void startOwnedDeviceDiscoveryProtocol(Identity ownedIdentity) throws Exception; + void startOwnedDeviceDiscoveryProtocolWithinTransaction(Session session, Identity ownedIdentity) throws Exception; + void startChannelCreationProtocolWithOwnedDevice(Session session, Identity ownedIdentity, UID ownedDeviceUid) throws Exception; void startTrustEstablishmentProtocol(Identity ownedIdentity, Identity contactIdentity, String contactDisplayName) throws Exception; void startMutualScanTrustEstablishmentProtocol(Identity ownedIdentity, Identity contactIdentity, byte[] signature) throws Exception; - void startChannelCreationWithContactDeviceProtocol(Identity ownedIdentity, Identity contactIdentity, UID contactDeviceUid) throws Exception; void startContactMutualIntroductionProtocol(Identity ownedIdentity, Identity contactIdentityA, Identity[] contactIdentities) throws Exception; void startGroupCreationProtocol(Identity ownedIdentity, String serializedGroupDetailsWithVersionAndPhoto, String photoUrl, HashSet groupMemberIdentitiesAndDisplayNames) throws Exception; - void startGroupV2CreationProtocol(Identity ownedIdentity, String serializedGroupDetails, String absolutePhotoUrl, HashSet ownPermissions, HashSet otherGroupMembers) throws Exception; + void startGroupV2CreationProtocol(Identity ownedIdentity, String serializedGroupDetails, String absolutePhotoUrl, HashSet ownPermissions, HashSet otherGroupMembers, String serializedGroupType) throws Exception; void initiateGroupV2Update(Identity ownedIdentity, GroupV2.Identifier groupIdentifier, ObvGroupV2.ObvGroupV2ChangeSet changeSet) throws Exception; void initiateGroupV2Leave(Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws Exception; void initiateGroupV2Disband(Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws Exception; void initiateGroupV2ReDownload(Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws Exception; void initiateGroupV2BatchKeysResend(Session session, Identity ownedIdentity, Identity contactIdentity, UID contactDeviceUid) throws Exception; void createOrUpdateKeycloakGroupV2(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier, String serializedKeycloakGroupBlob) throws Exception; + void processDeviceManagementRequest(Session session, Identity ownedIdentity, ObvDeviceManagementRequest deviceManagementRequest) throws Exception; void startIdentityDetailsPublicationProtocol(Session session, Identity ownedIdentity, int version) throws Exception; void startGroupDetailsPublicationProtocol(Session session, Identity ownedIdentity, byte[] groupUid) throws Exception; @@ -60,7 +65,7 @@ public interface ProtocolStarterDelegate { void startProtocolForBindingOwnedIdentityToKeycloakWithinTransaction(Session session, Identity ownedIdentity, ObvKeycloakState keycloakState, String keycloakUserId) throws Exception; void updateCurrentDeviceCapabilitiesForOwnedIdentity(Session session, Identity ownedIdentity, List newOwnCapabilities) throws Exception; void startProtocolForUnbindingOwnedIdentityFromKeycloak(Identity ownedIdentity) throws Exception; - void deleteOwnedIdentityAndNotifyContacts(Session session, Identity ownedIdentity) throws Exception; + void startOwnedIdentityDeletionProtocol(Session session, Identity ownedIdentity, boolean deleteEverywhere) throws Exception; void inviteContactsToGroup(byte[] groupOwnerAndUid, Identity ownedIdentity, HashSet newMembersIdentity) throws Exception; void reinvitePendingToGroup(byte[] groupOwnerAndUid, Identity ownedIdentity, Identity pendingMemberIdentity) throws Exception; @@ -75,4 +80,7 @@ public interface ProtocolStarterDelegate { void startDownloadGroupV2PhotoProtocolWithinTransaction(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier, GroupV2.ServerPhotoInfo serverPhotoInfo) throws Exception; void initiateGroupV2ReDownloadWithinTransaction(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier) throws Exception; void initiateKeycloakGroupV2TargetedPing(Session session, Identity ownedIdentity, GroupV2.Identifier groupIdentifier, Identity contactIdentity) throws Exception; + + void initiateSingleItemSync(Session session, Identity ownedIdentity, ObvSyncAtom obvSyncAtom) throws Exception; +// void triggerOwnedDevicesSync(Session session, Identity ownedIdentity); } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocol_engine/ConcreteProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocol_engine/ConcreteProtocol.java index c63382b9..24087d65 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocol_engine/ConcreteProtocol.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocol_engine/ConcreteProtocol.java @@ -33,6 +33,7 @@ import io.olvid.engine.protocol.databases.ReceivedMessage; import io.olvid.engine.protocol.datatypes.ProtocolManagerSession; import io.olvid.engine.protocol.protocols.ChannelCreationWithContactDeviceProtocol; +import io.olvid.engine.protocol.protocols.ChannelCreationWithOwnedDeviceProtocol; import io.olvid.engine.protocol.protocols.DeviceCapabilitiesDiscoveryProtocol; import io.olvid.engine.protocol.protocols.ContactMutualIntroductionProtocol; import io.olvid.engine.protocol.protocols.DeviceDiscoveryChildProtocol; @@ -49,7 +50,10 @@ import io.olvid.engine.protocol.protocols.KeycloakContactAdditionProtocol; import io.olvid.engine.protocol.protocols.ContactManagementProtocol; import io.olvid.engine.protocol.protocols.OneToOneContactInvitationProtocol; -import io.olvid.engine.protocol.protocols.OwnedIdentityDeletionWithContactNotificationProtocol; +import io.olvid.engine.protocol.protocols.OwnedDeviceDiscoveryProtocol; +import io.olvid.engine.protocol.protocols.OwnedDeviceManagementProtocol; +import io.olvid.engine.protocol.protocols.OwnedIdentityDeletionProtocol; +import io.olvid.engine.protocol.protocols.SynchronizationProtocol; import io.olvid.engine.protocol.protocols.TrustEstablishmentWithMutualScanProtocol; import io.olvid.engine.protocol.protocols.TrustEstablishmentWithSasProtocol; @@ -77,10 +81,15 @@ public abstract class ConcreteProtocol { public static final int ONE_TO_ONE_CONTACT_INVITATION_PROTOCOL_ID = 17; public static final int GROUPS_V2_PROTOCOL_ID = 18; public static final int DOWNLOAD_GROUPS_V2_PHOTO_PROTOCOL_ID = 19; - public static final int OWNED_IDENTITY_DELETION_WITH_CONTACT_NOTIFICATION_PROTOCOL_ID = 20; + public static final int OWNED_IDENTITY_DELETION_PROTOCOL_ID = 20; + public static final int OWNED_DEVICE_DISCOVERY_PROTOCOL_ID = 21; + public static final int CHANNEL_CREATION_WITH_OWNED_DEVICE_PROTOCOL_ID = 22; + public static final int KEYCLOAK_BINDING_AND_UNBINDING_PROTOCOL_ID = 23; + public static final int OWNED_DEVICE_MANAGEMENT_PROTOCOL_ID = 24; + public static final int SYNCHRONIZATION_PROTOCOL_ID = 25; // internal protocols, Android only - public static final int KEYCLOAK_BINDING_AND_UNBINDING_PROTOCOL_ID = 1000; + public static final int LEGACY_KEYCLOAK_BINDING_AND_UNBINDING_PROTOCOL_ID = 1000; protected final ProtocolManagerSession protocolManagerSession; @@ -191,16 +200,25 @@ private static ConcreteProtocol getConcreteProtocol(ProtocolManagerSession proto return new KeycloakContactAdditionProtocol(protocolManagerSession, protocolInstanceUid, stateId, encodedState, ownedIdentity, prng, jsonObjectMapper); case DEVICE_CAPABILITIES_DISCOVERY_PROTOCOL_ID: return new DeviceCapabilitiesDiscoveryProtocol(protocolManagerSession, protocolInstanceUid, stateId, encodedState, ownedIdentity, prng, jsonObjectMapper); + case LEGACY_KEYCLOAK_BINDING_AND_UNBINDING_PROTOCOL_ID: case KEYCLOAK_BINDING_AND_UNBINDING_PROTOCOL_ID: return new KeycloakBindingAndUnbindingProtocol(protocolManagerSession, protocolInstanceUid, stateId, encodedState, ownedIdentity, prng, jsonObjectMapper); - case OWNED_IDENTITY_DELETION_WITH_CONTACT_NOTIFICATION_PROTOCOL_ID: - return new OwnedIdentityDeletionWithContactNotificationProtocol(protocolManagerSession, protocolInstanceUid, stateId, encodedState, ownedIdentity, prng, jsonObjectMapper); case ONE_TO_ONE_CONTACT_INVITATION_PROTOCOL_ID: return new OneToOneContactInvitationProtocol(protocolManagerSession, protocolInstanceUid, stateId, encodedState, ownedIdentity, prng, jsonObjectMapper); case GROUPS_V2_PROTOCOL_ID: return new GroupsV2Protocol(protocolManagerSession, protocolInstanceUid, stateId, encodedState, ownedIdentity, prng, jsonObjectMapper); case DOWNLOAD_GROUPS_V2_PHOTO_PROTOCOL_ID: return new DownloadGroupV2PhotoProtocol(protocolManagerSession, protocolInstanceUid, stateId, encodedState, ownedIdentity, prng, jsonObjectMapper); + case OWNED_IDENTITY_DELETION_PROTOCOL_ID: + return new OwnedIdentityDeletionProtocol(protocolManagerSession, protocolInstanceUid, stateId, encodedState, ownedIdentity, prng, jsonObjectMapper); + case OWNED_DEVICE_DISCOVERY_PROTOCOL_ID: + return new OwnedDeviceDiscoveryProtocol(protocolManagerSession, protocolInstanceUid, stateId, encodedState, ownedIdentity, prng, jsonObjectMapper); + case CHANNEL_CREATION_WITH_OWNED_DEVICE_PROTOCOL_ID: + return new ChannelCreationWithOwnedDeviceProtocol(protocolManagerSession, protocolInstanceUid, stateId, encodedState, ownedIdentity, prng, jsonObjectMapper); + case OWNED_DEVICE_MANAGEMENT_PROTOCOL_ID: + return new OwnedDeviceManagementProtocol(protocolManagerSession, protocolInstanceUid, stateId, encodedState, ownedIdentity, prng, jsonObjectMapper); + case SYNCHRONIZATION_PROTOCOL_ID: + return new SynchronizationProtocol(protocolManagerSession, protocolInstanceUid, stateId, encodedState, ownedIdentity, prng, jsonObjectMapper); default: Logger.w("Unknown protocol id: " + protocolId); return null; @@ -259,6 +277,7 @@ public final ProtocolStep getStepToExecute(ConcreteProtocolMessage concreteProto } return (ProtocolStep) constructor.newInstance(currentState, concreteProtocolMessage, this); } catch (Exception e) { + e.printStackTrace(); return null; } } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocol_engine/ProtocolOperation.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocol_engine/ProtocolOperation.java index 484fdf65..ff2a8b27 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocol_engine/ProtocolOperation.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocol_engine/ProtocolOperation.java @@ -140,7 +140,7 @@ public void doExecute() { // run the step - Logger.d("Executing step " + stepToExecute.getClass()); + Logger.d("Executing step " + stepToExecute.getClass().getName() + "\n - state: " + protocol.currentState.getClass().getName() + "\n - message: " + concreteProtocolMessage.getClass().getName()); OperationQueue queue = new OperationQueue(); queue.queue(stepToExecute); queue.execute(1, "Engine-ProtocolOperation"); @@ -152,7 +152,7 @@ public void doExecute() { cancel(RFC_THE_STEP_TO_EXECUTE_FAILED); return; } - Logger.d("Finished step " + stepToExecute.getClass() + ". It reached state " + stepToExecute.getEndState().getClass()); + Logger.d("Finished step " + stepToExecute.getClass().getName() + ". It reached state " + stepToExecute.getEndState().getClass().getName()); ConcreteProtocolState endState = stepToExecute.getEndState(); protocolInstance.updateCurrentState(endState); diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocol_engine/ProtocolStep.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocol_engine/ProtocolStep.java index 984f1b6b..9d5aa40e 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocol_engine/ProtocolStep.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocol_engine/ProtocolStep.java @@ -20,6 +20,8 @@ package io.olvid.engine.protocol.protocol_engine; +import java.util.Objects; + import io.olvid.engine.Logger; import io.olvid.engine.crypto.PRNGService; import io.olvid.engine.datatypes.Identity; @@ -41,7 +43,7 @@ public ConcreteProtocolState getEndState() { public ProtocolStep(ReceptionChannelInfo expectedReceptionChannelInfo, ConcreteProtocolMessage receivedMessage, ConcreteProtocol protocol) throws Exception { if (expectedReceptionChannelInfo.getChannelType() == ReceptionChannelInfo.ANY_OBLIVIOUS_CHANNEL_WITH_OWNED_DEVICE_TYPE) { if ((receivedMessage.getReceptionChannelInfo().getChannelType() != ReceptionChannelInfo.OBLIVIOUS_CHANNEL_TYPE) || - (receivedMessage.getReceptionChannelInfo().getRemoteIdentity() != getOwnedIdentity())) { + (!Objects.equals(receivedMessage.getReceptionChannelInfo().getRemoteIdentity(), protocol.getOwnedIdentity()))) { Logger.d("Protocol expected ReceptionChannelInfo mismatch."); throw new Exception(); } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/ChannelCreationWithContactDeviceProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/ChannelCreationWithContactDeviceProtocol.java index afdf38ba..bd1a7207 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/ChannelCreationWithContactDeviceProtocol.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/ChannelCreationWithContactDeviceProtocol.java @@ -75,7 +75,6 @@ public int getProtocolId() { static final int WAITING_FOR_K1_STATE_ID = 3; static final int WAITING_FOR_K2_STATE_ID = 4; static final int WAIT_FOR_FIRST_ACK_STATE_ID = 5; - static final int WAIT_UNTIL_CONTACT_IS_TRUSTED_STATE_ID = 6; static final int WAIT_FOR_SECOND_ACK_STATE_ID = 7; static final int CHANNEL_CONFIRMED_STATE_ID = 8; @@ -246,39 +245,6 @@ public Encoded encode() { } - public static class WaitUntilContactIsTrustedState extends ConcreteProtocolState { - private final Identity contactIdentity; - private final UID contactDeviceUid; - private final Seed seed; - - public WaitUntilContactIsTrustedState(Encoded encodedState) throws Exception { - super(WAIT_UNTIL_CONTACT_IS_TRUSTED_STATE_ID); - Encoded[] list = encodedState.decodeList(); - if (list.length != 3) { - throw new Exception(); - } - this.contactIdentity = list[0].decodeIdentity(); - this.contactDeviceUid = list[1].decodeUid(); - this.seed = list[2].decodeSeed(); - } - - WaitUntilContactIsTrustedState(Identity contactIdentity, UID contactDeviceUid, Seed seed) { - super(WAIT_UNTIL_CONTACT_IS_TRUSTED_STATE_ID); - this.contactIdentity = contactIdentity; - this.contactDeviceUid = contactDeviceUid; - this.seed = seed; - } - - @Override - public Encoded encode() { - return Encoded.of(new Encoded[]{ - Encoded.of(contactIdentity), - Encoded.of(contactDeviceUid), - Encoded.of(seed), - }); - } - } - public static class WaitForSecondAckState extends ConcreteProtocolState { private final Identity contactIdentity; @@ -788,8 +754,6 @@ public ConcreteProtocolState executeStep() throws Exception { // Compute a signature to prove we trust the contact and don't have any channel/ongoing protocol with him - - // only rewrite the end of the prefix byte[] signature = protocolManagerSession.identityDelegate.signChannel( protocolManagerSession.session, Constants.SignatureContext.CHANNEL_CREATION, @@ -999,7 +963,7 @@ public ConcreteProtocolState executeStep() throws Exception { // Add the contactDeviceUid to the contactIdentity if needed --> If the device was indeed added, trigger a device discovery try { - if (protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), startState.contactIdentity, startState.contactDeviceUid)) { + if (protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), startState.contactIdentity, startState.contactDeviceUid, true)) { CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), ConcreteProtocol.DEVICE_DISCOVERY_PROTOCOL_ID, new UID(getPrng()), @@ -1074,7 +1038,7 @@ public ConcreteProtocolState executeStep() throws Exception { // Add the contactDeviceUid to the contactIdentity if needed --> If the device was indeed added, trigger a device discovery try { - if (protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), startState.contactIdentity, startState.contactDeviceUid)) { + if (protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), startState.contactIdentity, startState.contactDeviceUid, true)) { CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), ConcreteProtocol.DEVICE_DISCOVERY_PROTOCOL_ID, new UID(getPrng()), diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/ChannelCreationWithOwnedDeviceProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/ChannelCreationWithOwnedDeviceProtocol.java new file mode 100644 index 00000000..a0830ca1 --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/ChannelCreationWithOwnedDeviceProtocol.java @@ -0,0 +1,1250 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.protocol.protocols; + + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.sql.SQLException; +import java.util.Objects; + +import io.olvid.engine.Logger; +import io.olvid.engine.crypto.PRNGService; +import io.olvid.engine.crypto.PublicKeyEncryption; +import io.olvid.engine.crypto.Signature; +import io.olvid.engine.crypto.Suite; +import io.olvid.engine.datatypes.Constants; +import io.olvid.engine.datatypes.EncryptedBytes; +import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.datatypes.Seed; +import io.olvid.engine.datatypes.UID; +import io.olvid.engine.datatypes.containers.ChannelMessageToSend; +import io.olvid.engine.datatypes.containers.CiphertextAndKey; +import io.olvid.engine.datatypes.containers.ReceptionChannelInfo; +import io.olvid.engine.datatypes.containers.SendChannelInfo; +import io.olvid.engine.datatypes.key.asymmetric.EncryptionPrivateKey; +import io.olvid.engine.datatypes.key.asymmetric.EncryptionPublicKey; +import io.olvid.engine.datatypes.key.asymmetric.KeyPair; +import io.olvid.engine.datatypes.key.symmetric.AuthEncKey; +import io.olvid.engine.encoder.Encoded; +import io.olvid.engine.engine.types.JsonIdentityDetailsWithVersionAndPhoto; +import io.olvid.engine.protocol.databases.ChannelCreationPingSignatureReceived; +import io.olvid.engine.protocol.databases.ChannelCreationProtocolInstance; +import io.olvid.engine.protocol.databases.ReceivedMessage; +import io.olvid.engine.protocol.datatypes.CoreProtocolMessage; +import io.olvid.engine.protocol.datatypes.ProtocolManagerSession; +import io.olvid.engine.protocol.protocol_engine.ConcreteProtocol; +import io.olvid.engine.protocol.protocol_engine.ConcreteProtocolMessage; +import io.olvid.engine.protocol.protocol_engine.ConcreteProtocolState; +import io.olvid.engine.protocol.protocol_engine.InitialProtocolState; +import io.olvid.engine.protocol.protocol_engine.ProtocolStep; + +public class ChannelCreationWithOwnedDeviceProtocol extends ConcreteProtocol { + public ChannelCreationWithOwnedDeviceProtocol(ProtocolManagerSession protocolManagerSession, UID protocolInstanceUid, int currentStateId, Encoded encodedCurrentState, Identity ownedIdentity, PRNGService prng, ObjectMapper jsonObjectMapper) throws Exception { + super(protocolManagerSession, protocolInstanceUid, currentStateId, encodedCurrentState, ownedIdentity, prng, jsonObjectMapper); + } + + @Override + public int getProtocolId() { + return CHANNEL_CREATION_WITH_OWNED_DEVICE_PROTOCOL_ID; + } + + + + + //region States + + static final int CANCELLED_STATE_ID = 1; + static final int PING_SENT_STATE_ID = 2; + static final int WAITING_FOR_K1_STATE_ID = 3; + static final int WAITING_FOR_K2_STATE_ID = 4; + static final int WAIT_FOR_FIRST_ACK_STATE_ID = 5; + static final int WAIT_FOR_SECOND_ACK_STATE_ID = 7; + static final int CHANNEL_CONFIRMED_STATE_ID = 8; + + @Override + public int[] getFinalStateIds() { + return new int[]{CANCELLED_STATE_ID, CHANNEL_CONFIRMED_STATE_ID, PING_SENT_STATE_ID}; + } + + @Override + public Class getStateClass(int stateId) { + switch (stateId) { + case INITIAL_STATE_ID: + return InitialProtocolState.class; + case CANCELLED_STATE_ID: + return CancelledState.class; + case PING_SENT_STATE_ID: + return PingSentState.class; + case WAITING_FOR_K1_STATE_ID: + return WaitingForK1State.class; + case WAITING_FOR_K2_STATE_ID: + return WaitingForK2State.class; + case WAIT_FOR_FIRST_ACK_STATE_ID: + return WaitForFirstAckState.class; + case WAIT_FOR_SECOND_ACK_STATE_ID: + return WaitForSecondAckState.class; + case CHANNEL_CONFIRMED_STATE_ID: + return ChannelConfirmedState.class; + default: + return null; + } + } + + public static class CancelledState extends ConcreteProtocolState { + public CancelledState(Encoded encodedState) throws Exception { + super(CANCELLED_STATE_ID); + Encoded[] list = encodedState.decodeList(); + if (list.length != 0) { + throw new Exception(); + } + } + CancelledState() { + super(CANCELLED_STATE_ID); + } + @Override + public Encoded encode() { + return Encoded.of(new Encoded[0]); + } + } + + + public static class PingSentState extends ConcreteProtocolState { + public PingSentState(Encoded encodedState) throws Exception { + super(PING_SENT_STATE_ID); + Encoded[] list = encodedState.decodeList(); + if (list.length != 0) { + throw new Exception(); + } + } + PingSentState() { + super(PING_SENT_STATE_ID); + } + @Override + public Encoded encode() { + return Encoded.of(new Encoded[0]); + } + } + + + public static class WaitingForK1State extends ConcreteProtocolState { + private final UID remoteDeviceUid; + private final EncryptionPrivateKey ephemeralPrivateKey; + + public WaitingForK1State(Encoded encodedState) throws Exception { + super(WAITING_FOR_K1_STATE_ID); + Encoded[] list = encodedState.decodeList(); + if (list.length != 2) { + throw new Exception(); + } + this.remoteDeviceUid = list[0].decodeUid(); + this.ephemeralPrivateKey = (EncryptionPrivateKey) list[1].decodePrivateKey(); + } + + WaitingForK1State(UID remoteDeviceUid, EncryptionPrivateKey ephemeralPrivateKey) { + super(WAITING_FOR_K1_STATE_ID); + this.remoteDeviceUid = remoteDeviceUid; + this.ephemeralPrivateKey = ephemeralPrivateKey; + } + + @Override + public Encoded encode() { + return Encoded.of(new Encoded[]{ + Encoded.of(remoteDeviceUid), + Encoded.of(ephemeralPrivateKey), + }); + } + } + + + public static class WaitingForK2State extends ConcreteProtocolState { + private final UID remoteDeviceUid; + private final EncryptionPrivateKey ephemeralPrivateKey; + private final AuthEncKey k1; + + public WaitingForK2State(Encoded encodedState) throws Exception { + super(WAITING_FOR_K2_STATE_ID); + Encoded[] list = encodedState.decodeList(); + if (list.length != 3) { + throw new Exception(); + } + this.remoteDeviceUid = list[0].decodeUid(); + this.ephemeralPrivateKey = (EncryptionPrivateKey) list[1].decodePrivateKey(); + this.k1 = (AuthEncKey) list[2].decodeSymmetricKey(); + } + + WaitingForK2State(UID remoteDeviceUid, EncryptionPrivateKey ephemeralPrivateKey, AuthEncKey k1) { + super(WAITING_FOR_K2_STATE_ID); + this.remoteDeviceUid = remoteDeviceUid; + this.ephemeralPrivateKey = ephemeralPrivateKey; + this.k1 = k1; + } + + @Override + public Encoded encode() { + return Encoded.of(new Encoded[]{ + Encoded.of(remoteDeviceUid), + Encoded.of(ephemeralPrivateKey), + Encoded.of(k1), + }); + } + } + + + public static class WaitForFirstAckState extends ConcreteProtocolState { + private final UID remoteDeviceUid; + + public WaitForFirstAckState(Encoded encodedState) throws Exception { + super(WAIT_FOR_FIRST_ACK_STATE_ID); + Encoded[] list = encodedState.decodeList(); + if (list.length != 1) { + throw new Exception(); + } + this.remoteDeviceUid = list[0].decodeUid(); + } + + WaitForFirstAckState(UID remoteDeviceUid) { + super(WAIT_FOR_FIRST_ACK_STATE_ID); + this.remoteDeviceUid = remoteDeviceUid; + } + + @Override + public Encoded encode() { + return Encoded.of(new Encoded[]{ + Encoded.of(remoteDeviceUid), + }); + } + } + public static class WaitForSecondAckState extends ConcreteProtocolState { + private final UID remoteDeviceUid; + + public WaitForSecondAckState(Encoded encodedState) throws Exception { + super(WAIT_FOR_SECOND_ACK_STATE_ID); + Encoded[] list = encodedState.decodeList(); + if (list.length != 1) { + throw new Exception(); + } + this.remoteDeviceUid = list[0].decodeUid(); + } + + WaitForSecondAckState(UID remoteDeviceUid) { + super(WAIT_FOR_SECOND_ACK_STATE_ID); + this.remoteDeviceUid = remoteDeviceUid; + } + + @Override + public Encoded encode() { + return Encoded.of(new Encoded[]{ + Encoded.of(remoteDeviceUid), + }); + } + } + + + public static class ChannelConfirmedState extends ConcreteProtocolState { + public ChannelConfirmedState(Encoded encodedState) throws Exception { + super(CHANNEL_CONFIRMED_STATE_ID); + Encoded[] list = encodedState.decodeList(); + if (list.length != 0) { + throw new Exception(); + } + } + + ChannelConfirmedState() { + super(CHANNEL_CONFIRMED_STATE_ID); + } + + @Override + public Encoded encode() { + return Encoded.of(new Encoded[0]); + } + } + + //endregion + + + + + + + + + + // region Messages + + static final int INITIAL_MESSAGE_ID = 0; + static final int PING_MESSAGE_ID = 1; + static final int ALICE_IDENTITY_AND_EPHEMERAL_KEY_MESSAGE_ID = 2; + static final int BOB_EPHEMERAL_KEY_AND_K1_MESSAGE_ID = 3; + static final int K2_MESSAGE_ID = 4; + static final int FIRST_ACK_MESSAGE_ID = 5; + static final int SECOND_ACK_MESSAGE_ID = 6; + + + @Override + protected Class getMessageClass(int protocolMessageId) { + switch (protocolMessageId) { + case INITIAL_MESSAGE_ID: + return InitialMessage.class; + case PING_MESSAGE_ID: + return PingMessage.class; + case ALICE_IDENTITY_AND_EPHEMERAL_KEY_MESSAGE_ID: + return AliceIdentityAndEphemeralKeyMessage.class; + case BOB_EPHEMERAL_KEY_AND_K1_MESSAGE_ID: + return BobEphemeralKeyAndK1Message.class; + case K2_MESSAGE_ID: + return K2Message.class; + case FIRST_ACK_MESSAGE_ID: + return FirstAckMessage.class; + case SECOND_ACK_MESSAGE_ID: + return SecondAckMessage.class; + default: + return null; + } + } + + + public static class InitialMessage extends ConcreteProtocolMessage { + private final UID remoteDeviceUid; + + public InitialMessage(CoreProtocolMessage coreProtocolMessage, UID remoteDeviceUid) { + super(coreProtocolMessage); + this.remoteDeviceUid = remoteDeviceUid; + } + + public InitialMessage(ReceivedMessage receivedMessage) throws Exception { + super(new CoreProtocolMessage(receivedMessage)); + if (receivedMessage.getInputs().length != 1) { + throw new Exception(); + } + this.remoteDeviceUid = receivedMessage.getInputs()[0].decodeUid(); + } + + @Override + public int getProtocolMessageId() { + return INITIAL_MESSAGE_ID; + } + + @Override + public Encoded[] getInputs() { + return new Encoded[]{ + Encoded.of(remoteDeviceUid), + }; + } + } + + + public static class PingMessage extends ConcreteProtocolMessage { + private final UID remoteDeviceUid; + private final byte[] signature; + + public PingMessage(CoreProtocolMessage coreProtocolMessage, UID remoteDeviceUid, byte[] signature) { + super(coreProtocolMessage); + this.remoteDeviceUid = remoteDeviceUid; + this.signature = signature; + } + + public PingMessage(ReceivedMessage receivedMessage) throws Exception { + super(new CoreProtocolMessage(receivedMessage)); + if (receivedMessage.getInputs().length != 2) { + throw new Exception(); + } + this.remoteDeviceUid = receivedMessage.getInputs()[0].decodeUid(); + this.signature = receivedMessage.getInputs()[1].decodeBytes(); + } + + @Override + public int getProtocolMessageId() { + return PING_MESSAGE_ID; + } + + @Override + public Encoded[] getInputs() { + return new Encoded[]{ + Encoded.of(remoteDeviceUid), + Encoded.of(signature), + }; + } + } + + + public static class AliceIdentityAndEphemeralKeyMessage extends ConcreteProtocolMessage { + private final UID remoteDeviceUid; + private final byte[] signature; + private final EncryptionPublicKey remoteEphemeralPublicKey; + + public AliceIdentityAndEphemeralKeyMessage(CoreProtocolMessage coreProtocolMessage, UID remoteDeviceUid, byte[] signature, EncryptionPublicKey remoteEphemeralPublicKey) { + super(coreProtocolMessage); + this.remoteDeviceUid = remoteDeviceUid; + this.signature = signature; + this.remoteEphemeralPublicKey = remoteEphemeralPublicKey; + } + + public AliceIdentityAndEphemeralKeyMessage(ReceivedMessage receivedMessage) throws Exception { + super(new CoreProtocolMessage(receivedMessage)); + if (receivedMessage.getInputs().length != 3) { + throw new Exception(); + } + this.remoteDeviceUid = receivedMessage.getInputs()[0].decodeUid(); + this.signature = receivedMessage.getInputs()[1].decodeBytes(); + this.remoteEphemeralPublicKey = (EncryptionPublicKey) receivedMessage.getInputs()[2].decodePublicKey(); + } + + @Override + public int getProtocolMessageId() { + return ALICE_IDENTITY_AND_EPHEMERAL_KEY_MESSAGE_ID; + } + + @Override + public Encoded[] getInputs() { + return new Encoded[]{ + Encoded.of(remoteDeviceUid), + Encoded.of(signature), + Encoded.of(remoteEphemeralPublicKey), + }; + } + } + + + public static class BobEphemeralKeyAndK1Message extends ConcreteProtocolMessage { + private final EncryptionPublicKey remoteEphemeralPublicKey; + private final EncryptedBytes c1; + + BobEphemeralKeyAndK1Message(CoreProtocolMessage coreProtocolMessage, EncryptionPublicKey remoteEphemeralPublicKey, EncryptedBytes c1) { + super(coreProtocolMessage); + this.remoteEphemeralPublicKey = remoteEphemeralPublicKey; + this.c1 = c1; + } + + public BobEphemeralKeyAndK1Message(ReceivedMessage receivedMessage) throws Exception { + super(new CoreProtocolMessage(receivedMessage)); + if (receivedMessage.getInputs().length != 2) { + throw new Exception(); + } + this.remoteEphemeralPublicKey = (EncryptionPublicKey) receivedMessage.getInputs()[0].decodePublicKey(); + this.c1 = receivedMessage.getInputs()[1].decodeEncryptedData(); + } + + @Override + public int getProtocolMessageId() { + return BOB_EPHEMERAL_KEY_AND_K1_MESSAGE_ID; + } + + @Override + public Encoded[] getInputs() { + return new Encoded[]{ + Encoded.of(remoteEphemeralPublicKey), + Encoded.of(c1), + }; + } + } + + + public static class K2Message extends ConcreteProtocolMessage { + private final EncryptedBytes c2; + + K2Message(CoreProtocolMessage coreProtocolMessage, EncryptedBytes c2) { + super(coreProtocolMessage); + this.c2 = c2; + } + + public K2Message(ReceivedMessage receivedMessage) throws Exception { + super(new CoreProtocolMessage(receivedMessage)); + if (receivedMessage.getInputs().length != 1) { + throw new Exception(); + } + this.c2 = receivedMessage.getInputs()[0].decodeEncryptedData(); + } + + @Override + public int getProtocolMessageId() { + return K2_MESSAGE_ID; + } + + @Override + public Encoded[] getInputs() { + return new Encoded[]{ + Encoded.of(c2), + }; + } + } + + + public static class FirstAckMessage extends ConcreteProtocolMessage { + private final String remoteSerializedIdentityWithVersionAndPhoto; + + FirstAckMessage(CoreProtocolMessage coreProtocolMessage, String remoteSerializedIdentityWithVersionAndPhoto) { + super(coreProtocolMessage); + this.remoteSerializedIdentityWithVersionAndPhoto = remoteSerializedIdentityWithVersionAndPhoto; + } + + public FirstAckMessage(ReceivedMessage receivedMessage) throws Exception { + super(new CoreProtocolMessage(receivedMessage)); + if (receivedMessage.getInputs().length != 1) { + throw new Exception(); + } + this.remoteSerializedIdentityWithVersionAndPhoto = receivedMessage.getInputs()[0].decodeString(); + } + + @Override + public int getProtocolMessageId() { + return FIRST_ACK_MESSAGE_ID; + } + + @Override + public Encoded[] getInputs() { + return new Encoded[]{ + Encoded.of(remoteSerializedIdentityWithVersionAndPhoto), + }; + } + } + + + public static class SecondAckMessage extends ConcreteProtocolMessage { + private final String remoteSerializedIdentityWithVersionAndPhoto; + + SecondAckMessage(CoreProtocolMessage coreProtocolMessage, String remoteSerializedIdentityWithVersionAndPhoto) { + super(coreProtocolMessage); + this.remoteSerializedIdentityWithVersionAndPhoto = remoteSerializedIdentityWithVersionAndPhoto; + } + + public SecondAckMessage(ReceivedMessage receivedMessage) throws Exception { + super(new CoreProtocolMessage(receivedMessage)); + if (receivedMessage.getInputs().length != 1) { + throw new Exception(); + } + this.remoteSerializedIdentityWithVersionAndPhoto = receivedMessage.getInputs()[0].decodeString(); + } + + @Override + public int getProtocolMessageId() { + return SECOND_ACK_MESSAGE_ID; + } + + @Override + public Encoded[] getInputs() { + return new Encoded[]{ + Encoded.of(remoteSerializedIdentityWithVersionAndPhoto), + }; + } + } + + //endregion + + + + + + + + + + //region Steps + + @Override + public Class[] getPossibleStepClasses(int stateId) { + switch (stateId) { + case INITIAL_STATE_ID: + return new Class[]{SendPingStep.class, SendPingOrEphemeralKeyStep.class, SendEphemeralKeyAndK1Step.class}; + case WAITING_FOR_K1_STATE_ID: + return new Class[]{RecoverK1AndSendK2AndCreateChannelStep.class}; + case WAITING_FOR_K2_STATE_ID: + return new Class[]{RecoverK2CreateChannelAndSendAckStep.class}; + case WAIT_FOR_FIRST_ACK_STATE_ID: + return new Class[]{ConfirmChannelAndSendAckStep.class}; + case WAIT_FOR_SECOND_ACK_STATE_ID: + return new Class[]{ConfirmChannelStep.class}; + case CANCELLED_STATE_ID: + case PING_SENT_STATE_ID: + case CHANNEL_CONFIRMED_STATE_ID: + default: + return new Class[0]; + } + } + + public static class SendPingStep extends ProtocolStep { + private final InitialProtocolState startState; + private final InitialMessage receivedMessage; + + public SendPingStep(InitialProtocolState startState, InitialMessage receivedMessage, ChannelCreationWithOwnedDeviceProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createLocalChannelInfo(), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + // check that the remoteDeviceUid in the receivedMessage is not our currentDeviceUid + UID currentDeviceUid = protocolManagerSession.identityDelegate.getCurrentDeviceUidOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); + if (currentDeviceUid == null) { + return new CancelledState(); + } + if (Objects.equals(currentDeviceUid, receivedMessage.remoteDeviceUid)) { + Logger.w("Trying to run a ChannelCreationWithOwnedDeviceProtocol with our currentDeviceUid"); + return new CancelledState(); + } + + // clean any ongoing instance of this protocol + ChannelCreationProtocolInstance channelCreationProtocolInstance = null; + try { + channelCreationProtocolInstance = ChannelCreationProtocolInstance.get( + protocolManagerSession, + receivedMessage.remoteDeviceUid, + getOwnedIdentity(), + getOwnedIdentity() + ); + } catch (SQLException ignored) {} + if (channelCreationProtocolInstance != null) { + channelCreationProtocolInstance.delete(); + protocolManagerSession.protocolDelegate.abortProtocol(protocolManagerSession.session, channelCreationProtocolInstance.getProtocolInstanceUid(), getOwnedIdentity()); + } + + // clear any already created ObliviousChannel + protocolManagerSession.channelDelegate.deleteObliviousChannelIfItExists( + protocolManagerSession.session, + getOwnedIdentity(), + receivedMessage.remoteDeviceUid, + getOwnedIdentity()); + + // send a signed ping + byte[] signature = protocolManagerSession.identityDelegate.signChannel( + protocolManagerSession.session, + Constants.SignatureContext.CHANNEL_CREATION, + getOwnedIdentity(), + receivedMessage.remoteDeviceUid, + getOwnedIdentity(), + currentDeviceUid, + getPrng() + ); + + // send the ping containing the signature + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAsymmetricChannelInfo(getOwnedIdentity(), getOwnedIdentity(), new UID[]{receivedMessage.remoteDeviceUid})); + ChannelMessageToSend messageToSend = new PingMessage(coreProtocolMessage, currentDeviceUid, signature).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + + return new PingSentState(); + } + } + + + + public static class SendPingOrEphemeralKeyStep extends ProtocolStep { + private final InitialProtocolState startState; + private final PingMessage receivedMessage; + + public SendPingOrEphemeralKeyStep(InitialProtocolState startState, PingMessage receivedMessage, ChannelCreationWithOwnedDeviceProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createAsymmetricChannelInfo(), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + // check that the remoteDeviceUid in the receivedMessage is not our currentDeviceUid + UID currentDeviceUid = protocolManagerSession.identityDelegate.getCurrentDeviceUidOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); + if (currentDeviceUid == null) { + return new CancelledState(); + } + if (Objects.equals(currentDeviceUid, receivedMessage.remoteDeviceUid)) { + Logger.w("Received a ping for a ChannelCreationWithOwnedDeviceProtocol with our currentDeviceUid"); + return new CancelledState(); + } + + // verify the signature in the PingMessage + boolean signatureIsValid = Signature.verify( + Constants.SignatureContext.CHANNEL_CREATION, + currentDeviceUid, + receivedMessage.remoteDeviceUid, + getOwnedIdentity(), + getOwnedIdentity(), + getOwnedIdentity(), + receivedMessage.signature + ); + + if (!signatureIsValid) { + return new CancelledState(); + } + + if (ChannelCreationPingSignatureReceived.exists(protocolManagerSession, getOwnedIdentity(), receivedMessage.signature)) { + // we already received a ping with the same signature! + return new CancelledState(); + } else { + // store the signature to prevent future replay + ChannelCreationPingSignatureReceived.create(protocolManagerSession, getOwnedIdentity(), receivedMessage.signature); + } + + // Signature is valid! The other device does not have a channel + { + // clean any ongoing instance of this protocol + ChannelCreationProtocolInstance channelCreationProtocolInstance = null; + try { + channelCreationProtocolInstance = ChannelCreationProtocolInstance.get( + protocolManagerSession, + receivedMessage.remoteDeviceUid, + getOwnedIdentity(), + getOwnedIdentity() + ); + } catch (SQLException ignored) {} + if (channelCreationProtocolInstance != null) { + channelCreationProtocolInstance.delete(); + protocolManagerSession.protocolDelegate.abortProtocol(protocolManagerSession.session, channelCreationProtocolInstance.getProtocolInstanceUid(), getOwnedIdentity()); + } + + // clear any already created ObliviousChannel + protocolManagerSession.channelDelegate.deleteObliviousChannelIfItExists( + protocolManagerSession.session, + getOwnedIdentity(), + receivedMessage.remoteDeviceUid, + getOwnedIdentity()); + } + + + + // Compute a signature to prove we don't have any channel/ongoing protocol with the other device + byte[] signature = protocolManagerSession.identityDelegate.signChannel( + protocolManagerSession.session, + Constants.SignatureContext.CHANNEL_CREATION, + getOwnedIdentity(), + receivedMessage.remoteDeviceUid, + getOwnedIdentity(), + currentDeviceUid, + getPrng() + ); + + + + // If we are in charge (small deviceUid), send an ephemeral key + // otherwise simply send a ping back + int compare = currentDeviceUid.compareTo(receivedMessage.remoteDeviceUid); + if (compare >= 0) { + // Not in charge + + // send the ping containing the signature + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAsymmetricChannelInfo(getOwnedIdentity(), getOwnedIdentity(), new UID[]{receivedMessage.remoteDeviceUid})); + ChannelMessageToSend messageToSend = new PingMessage(coreProtocolMessage, currentDeviceUid, signature).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + + return new PingSentState(); + } else { + // In charge + // Create a new ChannelCreationProtocolInstance + ChannelCreationProtocolInstance channelCreationProtocolInstance = ChannelCreationProtocolInstance.create( + protocolManagerSession, + receivedMessage.remoteDeviceUid, + getOwnedIdentity(), + getOwnedIdentity(), + getProtocolInstanceUid() + ); + if (channelCreationProtocolInstance == null) { + throw new Exception(); + } + + KeyPair keyPair = Suite.generateEncryptionKeyPair(getOwnedIdentity().getEncryptionPublicKey().getAlgorithmImplementation(), getPrng()); + if (keyPair == null) { + throw new Exception(); + } + + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAsymmetricChannelInfo(getOwnedIdentity(), getOwnedIdentity(), new UID[]{receivedMessage.remoteDeviceUid})); + ChannelMessageToSend messageToSend = new AliceIdentityAndEphemeralKeyMessage(coreProtocolMessage, currentDeviceUid, signature, (EncryptionPublicKey) keyPair.getPublicKey()).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + + return new WaitingForK1State(receivedMessage.remoteDeviceUid, (EncryptionPrivateKey) keyPair.getPrivateKey()); + } + } + } + + + public static class SendEphemeralKeyAndK1Step extends ProtocolStep { + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private final InitialProtocolState startState; + private final AliceIdentityAndEphemeralKeyMessage receivedMessage; + + public SendEphemeralKeyAndK1Step(InitialProtocolState startState, AliceIdentityAndEphemeralKeyMessage receivedMessage, ChannelCreationWithOwnedDeviceProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createAsymmetricChannelInfo(), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + { + // check that the remoteDeviceUid in the receivedMessage is not our currentDeviceUid + UID currentDeviceUid = protocolManagerSession.identityDelegate.getCurrentDeviceUidOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); + if (currentDeviceUid == null) { + return new CancelledState(); + } + if (Objects.equals(currentDeviceUid, receivedMessage.remoteDeviceUid)) { + Logger.w("Received a ping for a ChannelCreationWithOwnedDeviceProtocol with our currentDeviceUid"); + return new CancelledState(); + } + + boolean signatureIsValid = Signature.verify( + Constants.SignatureContext.CHANNEL_CREATION, + currentDeviceUid, + receivedMessage.remoteDeviceUid, + getOwnedIdentity(), + getOwnedIdentity(), + getOwnedIdentity(), + receivedMessage.signature + ); + + if (!signatureIsValid) { + return new CancelledState(); + } + + if (ChannelCreationPingSignatureReceived.exists(protocolManagerSession, getOwnedIdentity(), receivedMessage.signature)) { + // we already received a ping with the same signature! + return new CancelledState(); + } else { + // store the signature to prevent future replay + ChannelCreationPingSignatureReceived.create(protocolManagerSession, getOwnedIdentity(), receivedMessage.signature); + } + } + + + { + // check whether there already is an instance of this protocol running + ChannelCreationProtocolInstance channelCreationProtocolInstance = null; + try { + channelCreationProtocolInstance = ChannelCreationProtocolInstance.get( + protocolManagerSession, + receivedMessage.remoteDeviceUid, + getOwnedIdentity(), + getOwnedIdentity() + ); + } catch (SQLException ignored) {} + if (channelCreationProtocolInstance != null) { + // an instance already exists, abort it, terminate this protocol, and restart it with a fresh ping + channelCreationProtocolInstance.delete(); + protocolManagerSession.protocolDelegate.abortProtocol(protocolManagerSession.session, channelCreationProtocolInstance.getProtocolInstanceUid(), getOwnedIdentity()); + + + UID childProtocolInstanceUid = new UID(getPrng()); + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage( + SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), + CHANNEL_CREATION_WITH_OWNED_DEVICE_PROTOCOL_ID, + childProtocolInstanceUid, + false + ); + ChannelMessageToSend messageToSend = new ChannelCreationWithOwnedDeviceProtocol.InitialMessage(coreProtocolMessage, receivedMessage.remoteDeviceUid).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + + return new CancelledState(); + } else { + // No previous instance of the protocol exists, create one + channelCreationProtocolInstance = ChannelCreationProtocolInstance.create( + protocolManagerSession, + receivedMessage.remoteDeviceUid, + getOwnedIdentity(), + getOwnedIdentity(), + getProtocolInstanceUid() + ); + if (channelCreationProtocolInstance == null) { + throw new Exception(); + } + } + } + + KeyPair keyPair = Suite.generateEncryptionKeyPair(getOwnedIdentity().getEncryptionPublicKey().getAlgorithmImplementation(), getPrng()); + if (keyPair == null) { + throw new Exception(); + } + + // compute k1 + PublicKeyEncryption publicKeyEncryption = Suite.getPublicKeyEncryption(receivedMessage.remoteEphemeralPublicKey); + CiphertextAndKey ciphertextAndKey = publicKeyEncryption.kemEncrypt(receivedMessage.remoteEphemeralPublicKey, getPrng()); + AuthEncKey k1 = ciphertextAndKey.getKey(); + EncryptedBytes c1 = ciphertextAndKey.getCiphertext(); + + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAsymmetricChannelInfo(getOwnedIdentity(), getOwnedIdentity(), new UID[]{receivedMessage.remoteDeviceUid})); + ChannelMessageToSend messageToSend = new BobEphemeralKeyAndK1Message(coreProtocolMessage, (EncryptionPublicKey) keyPair.getPublicKey(), c1).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + + return new WaitingForK2State(receivedMessage.remoteDeviceUid, (EncryptionPrivateKey) keyPair.getPrivateKey(), k1); + } + } + + + public static class RecoverK1AndSendK2AndCreateChannelStep extends ProtocolStep { + private final WaitingForK1State startState; + private final BobEphemeralKeyAndK1Message receivedMessage; + + public RecoverK1AndSendK2AndCreateChannelStep(WaitingForK1State startState, BobEphemeralKeyAndK1Message receivedMessage, ChannelCreationWithOwnedDeviceProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createAsymmetricChannelInfo(), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + PublicKeyEncryption publicKeyEncryption = Suite.getPublicKeyEncryption(startState.ephemeralPrivateKey); + AuthEncKey k1 = publicKeyEncryption.kemDecrypt(startState.ephemeralPrivateKey, receivedMessage.c1); + if (k1 == null) { + Logger.e("Could not recover k1."); + return new CancelledState(); + } + + // compute k2 + publicKeyEncryption = Suite.getPublicKeyEncryption(receivedMessage.remoteEphemeralPublicKey); + CiphertextAndKey ciphertextAndKey = publicKeyEncryption.kemEncrypt(receivedMessage.remoteEphemeralPublicKey, getPrng()); + AuthEncKey k2 = ciphertextAndKey.getKey(); + EncryptedBytes c2 = ciphertextAndKey.getCiphertext(); + + Seed seed = Seed.of(k1, k2); + + // add the contact deviceUid if not already there + try { + protocolManagerSession.identityDelegate.addDeviceForOwnedIdentity(protocolManagerSession.session, getOwnedIdentity(), startState.remoteDeviceUid, null, null, null, true); + + // trigger an owned device discovery + UID protocolInstanceUid = new UID(getPrng()); + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), + ConcreteProtocol.OWNED_DEVICE_DISCOVERY_PROTOCOL_ID, + protocolInstanceUid, + false); + ChannelMessageToSend message = new OwnedDeviceDiscoveryProtocol.InitialMessage(coreProtocolMessage).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message, getPrng()); + } catch (Exception e) { + Logger.w("Exception when adding an owned device"); + } + + // if there is already a channel, we have a problem! Abort the protocol and restart from scratch + if (protocolManagerSession.channelDelegate.checkIfObliviousChannelExists(protocolManagerSession.session, getOwnedIdentity(), startState.remoteDeviceUid, getOwnedIdentity())) { + UID childProtocolInstanceUid = new UID(getPrng()); + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage( + SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), + CHANNEL_CREATION_WITH_OWNED_DEVICE_PROTOCOL_ID, + childProtocolInstanceUid, + false + ); + ChannelMessageToSend messageToSend = new ChannelCreationWithOwnedDeviceProtocol.InitialMessage(coreProtocolMessage, startState.remoteDeviceUid).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + + return new CancelledState(); + } + + // create the channel + protocolManagerSession.channelDelegate.createObliviousChannel( + protocolManagerSession.session, + getOwnedIdentity(), + startState.remoteDeviceUid, + getOwnedIdentity(), + seed, + 0 + ); + + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAsymmetricChannelInfo(getOwnedIdentity(), getOwnedIdentity(), new UID[]{startState.remoteDeviceUid})); + ChannelMessageToSend messageToSend = new K2Message(coreProtocolMessage, c2).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + + return new WaitForFirstAckState(startState.remoteDeviceUid); + } + } + + + public static class RecoverK2CreateChannelAndSendAckStep extends ProtocolStep { + private final WaitingForK2State startState; + private final K2Message receivedMessage; + + public RecoverK2CreateChannelAndSendAckStep(WaitingForK2State startState, K2Message receivedMessage, ChannelCreationWithOwnedDeviceProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createAsymmetricChannelInfo(), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + PublicKeyEncryption publicKeyEncryption = Suite.getPublicKeyEncryption(startState.ephemeralPrivateKey); + AuthEncKey k2 = publicKeyEncryption.kemDecrypt(startState.ephemeralPrivateKey, receivedMessage.c2); + if (k2 == null) { + Logger.e("Could not recover k2."); + return new CancelledState(); + } + + // Add the otherDeviceUid to the contactIdentity if needed --> we no longer trigger a device discovery + try { + protocolManagerSession.identityDelegate.addDeviceForOwnedIdentity(protocolManagerSession.session, getOwnedIdentity(), startState.remoteDeviceUid, null, null, null, true); + + // trigger an owned device discovery + UID protocolInstanceUid = new UID(getPrng()); + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), + ConcreteProtocol.OWNED_DEVICE_DISCOVERY_PROTOCOL_ID, + protocolInstanceUid, + false); + ChannelMessageToSend message = new OwnedDeviceDiscoveryProtocol.InitialMessage(coreProtocolMessage).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message, getPrng()); + } catch (Exception e) { + Logger.w("Exception when adding an owned device"); + } + + // if there is already a channel, we have a problem! Abort the protocol and restart from scratch + if (protocolManagerSession.channelDelegate.checkIfObliviousChannelExists(protocolManagerSession.session, getOwnedIdentity(), startState.remoteDeviceUid, getOwnedIdentity())) { + UID childProtocolInstanceUid = new UID(getPrng()); + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage( + SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), + CHANNEL_CREATION_WITH_OWNED_DEVICE_PROTOCOL_ID, + childProtocolInstanceUid, + false + ); + ChannelMessageToSend messageToSend = new ChannelCreationWithOwnedDeviceProtocol.InitialMessage(coreProtocolMessage, startState.remoteDeviceUid).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + + return new CancelledState(); + } + + + Seed seed = Seed.of(startState.k1, k2); + protocolManagerSession.channelDelegate.createObliviousChannel( + protocolManagerSession.session, + getOwnedIdentity(), + startState.remoteDeviceUid, + getOwnedIdentity(), + seed, + 0 + ); + + String serializedDetailsWithVersionAndPhoto = ""; + try { + JsonIdentityDetailsWithVersionAndPhoto ownedDetailsWithVersionAndPhoto = protocolManagerSession.identityDelegate.getOwnedIdentityPublishedAndLatestDetails(protocolManagerSession.session, getOwnedIdentity())[0]; + serializedDetailsWithVersionAndPhoto = protocol.getJsonObjectMapper().writeValueAsString(ownedDetailsWithVersionAndPhoto); + } catch (Exception e) { + e.printStackTrace(); + } + + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createObliviousChannelInfo( + getOwnedIdentity(), + getOwnedIdentity(), + new UID[]{startState.remoteDeviceUid}, + false + )); + ChannelMessageToSend messageToSend = new FirstAckMessage(coreProtocolMessage, serializedDetailsWithVersionAndPhoto).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + + return new WaitForSecondAckState(startState.remoteDeviceUid); + } + } + + + public static class ConfirmChannelAndSendAckStep extends ProtocolStep { + private final WaitForFirstAckState startState; + private final FirstAckMessage receivedMessage; + + public ConfirmChannelAndSendAckStep(WaitForFirstAckState startState, FirstAckMessage receivedMessage, ChannelCreationWithOwnedDeviceProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createObliviousChannelInfo(startState.remoteDeviceUid, protocol.ownedIdentity), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + { + // update the publishedContactDetails with what we just received + try { + JsonIdentityDetailsWithVersionAndPhoto ownDetailsWithVersionAndPhoto = protocol.getJsonObjectMapper().readValue(receivedMessage.remoteSerializedIdentityWithVersionAndPhoto, JsonIdentityDetailsWithVersionAndPhoto.class); + if (ownDetailsWithVersionAndPhoto != null) { + boolean photoDownloadNeeded = protocolManagerSession.identityDelegate.setOwnedIdentityDetailsFromOtherDevice(protocolManagerSession.session, getOwnedIdentity(), ownDetailsWithVersionAndPhoto); + + if (photoDownloadNeeded) { + // we need to download a photo + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage( + SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), + DOWNLOAD_IDENTITY_PHOTO_CHILD_PROTOCOL_ID, + new UID(getPrng()), + false + ); + ChannelMessageToSend messageToSend = new DownloadIdentityPhotoChildProtocol.InitialMessage(coreProtocolMessage, getOwnedIdentity(), receivedMessage.remoteSerializedIdentityWithVersionAndPhoto).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + { + // We received a message on the obliviousChannel, so we can confirm it + protocolManagerSession.channelDelegate.confirmObliviousChannel( + protocolManagerSession.session, + getOwnedIdentity(), + startState.remoteDeviceUid, + getOwnedIdentity() + ); + } + + { + // send this device capabilities to other device + UID childProtocolInstanceUid = new UID(getPrng()); + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage( + SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), + DEVICE_CAPABILITIES_DISCOVERY_PROTOCOL_ID, + childProtocolInstanceUid, + false + ); + ChannelMessageToSend messageToSend = new DeviceCapabilitiesDiscoveryProtocol.InitialSingleOwnedDeviceMessage(coreProtocolMessage, startState.remoteDeviceUid, false).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } + + { + // Delete the ChannelCreationProtocolInstance + try { + ChannelCreationProtocolInstance channelCreationProtocolInstance = ChannelCreationProtocolInstance.get(protocolManagerSession, startState.remoteDeviceUid, getOwnedIdentity(), getOwnedIdentity()); + channelCreationProtocolInstance.delete(); + } catch (Exception e) { + Logger.w("Exception when deleting a ChannelCreationProtocolInstance"); + } + } + + { + // send Ack message to Bob + String serializedDetailsWithVersionAndPhoto = ""; + try { + JsonIdentityDetailsWithVersionAndPhoto ownedDetailsWithVersionAndPhoto = protocolManagerSession.identityDelegate.getOwnedIdentityPublishedAndLatestDetails(protocolManagerSession.session, getOwnedIdentity())[0]; + serializedDetailsWithVersionAndPhoto = protocol.getJsonObjectMapper().writeValueAsString(ownedDetailsWithVersionAndPhoto); + } catch (Exception e) { + e.printStackTrace(); + } + + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createObliviousChannelInfo( + getOwnedIdentity(), + getOwnedIdentity(), + new UID[]{startState.remoteDeviceUid}, + true + )); + ChannelMessageToSend messageToSend = new SecondAckMessage(coreProtocolMessage, serializedDetailsWithVersionAndPhoto).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } + +// { +// // initiate a device synchronization protocol +// UID currentDeviceUid = protocolManagerSession.identityDelegate.getCurrentDeviceUidOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); +// CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), +// ConcreteProtocol.SYNCHRONIZATION_PROTOCOL_ID, +// SynchronizationProtocol.computeOngoingProtocolInstanceUid(getOwnedIdentity(), currentDeviceUid, startState.remoteDeviceUid), +// false); +// ChannelMessageToSend message = new SynchronizationProtocol.InitiateSyncMessage(coreProtocolMessage, startState.remoteDeviceUid).generateChannelProtocolMessageToSend(); +// protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message, getPrng()); +// } + + return new ChannelConfirmedState(); + } + } + + public static class ConfirmChannelStep extends ProtocolStep { + private final WaitForSecondAckState startState; + private final SecondAckMessage receivedMessage; + + public ConfirmChannelStep(WaitForSecondAckState startState, SecondAckMessage receivedMessage, ChannelCreationWithOwnedDeviceProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createObliviousChannelInfo(startState.remoteDeviceUid, protocol.ownedIdentity), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + + // update the publishedContactDetails with what we just received + { + // update the publishedContactDetails with what we just received + try { + JsonIdentityDetailsWithVersionAndPhoto ownDetailsWithVersionAndPhoto = protocol.getJsonObjectMapper().readValue(receivedMessage.remoteSerializedIdentityWithVersionAndPhoto, JsonIdentityDetailsWithVersionAndPhoto.class); + if (ownDetailsWithVersionAndPhoto != null) { + boolean photoDownloadNeeded = protocolManagerSession.identityDelegate.setOwnedIdentityDetailsFromOtherDevice(protocolManagerSession.session, getOwnedIdentity(), ownDetailsWithVersionAndPhoto); + + if (photoDownloadNeeded) { + // we need to download a photo + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage( + SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), + DOWNLOAD_IDENTITY_PHOTO_CHILD_PROTOCOL_ID, + new UID(getPrng()), + false + ); + ChannelMessageToSend messageToSend = new DownloadIdentityPhotoChildProtocol.InitialMessage(coreProtocolMessage, getOwnedIdentity(), receivedMessage.remoteSerializedIdentityWithVersionAndPhoto).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + { + // we can confirm the obliviousChannel + protocolManagerSession.channelDelegate.confirmObliviousChannel( + protocolManagerSession.session, + getOwnedIdentity(), + startState.remoteDeviceUid, + getOwnedIdentity() + ); + } + + { + // send this device capabilities to other device + UID childProtocolInstanceUid = new UID(getPrng()); + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage( + SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), + DEVICE_CAPABILITIES_DISCOVERY_PROTOCOL_ID, + childProtocolInstanceUid, + false + ); + ChannelMessageToSend messageToSend = new DeviceCapabilitiesDiscoveryProtocol.InitialSingleOwnedDeviceMessage(coreProtocolMessage, startState.remoteDeviceUid, false).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } + + + // Delete the ChannelCreationProtocolInstance + try { + ChannelCreationProtocolInstance channelCreationProtocolInstance = ChannelCreationProtocolInstance.get(protocolManagerSession, startState.remoteDeviceUid, getOwnedIdentity(), getOwnedIdentity()); + channelCreationProtocolInstance.delete(); + } catch (Exception e) { + Logger.w("Exception when deleting a ChannelCreationProtocolInstance"); + } + +// { +// // initiate a device synchronization protocol +// UID currentDeviceUid = protocolManagerSession.identityDelegate.getCurrentDeviceUidOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); +// CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), +// ConcreteProtocol.SYNCHRONIZATION_PROTOCOL_ID, +// SynchronizationProtocol.computeOngoingProtocolInstanceUid(getOwnedIdentity(), currentDeviceUid, startState.remoteDeviceUid), +// false); +// ChannelMessageToSend message = new SynchronizationProtocol.InitiateSyncMessage(coreProtocolMessage, startState.remoteDeviceUid).generateChannelProtocolMessageToSend(); +// protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message, getPrng()); +// } + + return new ChannelConfirmedState(); + } + } + + //endregion + +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/ContactManagementProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/ContactManagementProtocol.java index dc00bafd..eb609098 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/ContactManagementProtocol.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/ContactManagementProtocol.java @@ -25,6 +25,7 @@ import io.olvid.engine.crypto.PRNGService; import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.datatypes.NoAcceptableChannelException; import io.olvid.engine.datatypes.UID; import io.olvid.engine.datatypes.containers.ChannelMessageToSend; import io.olvid.engine.datatypes.containers.GroupInformation; @@ -37,6 +38,7 @@ import io.olvid.engine.protocol.protocol_engine.ConcreteProtocol; import io.olvid.engine.protocol.protocol_engine.ConcreteProtocolMessage; import io.olvid.engine.protocol.protocol_engine.ConcreteProtocolState; +import io.olvid.engine.protocol.protocol_engine.EmptyProtocolMessage; import io.olvid.engine.protocol.protocol_engine.InitialProtocolState; import io.olvid.engine.protocol.protocol_engine.ProtocolStep; @@ -112,6 +114,7 @@ public Encoded encode() { private static final int INITIATE_CONTACT_DOWNGRADE_MESSAGE_ID = 3; private static final int CONTACT_DOWNGRADE_NOTIFICATION_MESSAGE_ID = 4; private static final int PROPAGATE_CONTACT_DOWNGRADE_MESSAGE_ID = 5; + private static final int PERFORM_CONTACT_DEVICE_DISCOVERY_MESSAGE_ID = 6; @Override @@ -129,6 +132,8 @@ protected Class getMessageClass(int protocolMessageId) { return ContactDowngradeNotificationMessage.class; case PROPAGATE_CONTACT_DOWNGRADE_MESSAGE_ID: return PropagateContactDowngradeMessage.class; + case PERFORM_CONTACT_DEVICE_DISCOVERY_MESSAGE_ID: + return PerformContactDeviceDiscoveryMessage.class; default: return null; } @@ -165,28 +170,20 @@ public Encoded[] getInputs() { } - public static class ContactDeletionNotificationMessage extends ConcreteProtocolMessage { + public static class ContactDeletionNotificationMessage extends EmptyProtocolMessage { public ContactDeletionNotificationMessage(CoreProtocolMessage coreProtocolMessage) { super(coreProtocolMessage); } @SuppressWarnings("unused") public ContactDeletionNotificationMessage(ReceivedMessage receivedMessage) throws Exception { - super(new CoreProtocolMessage(receivedMessage)); - if (receivedMessage.getInputs().length != 0) { - throw new Exception(); - } + super(receivedMessage); } @Override public int getProtocolMessageId() { return CONTACT_DELETION_NOTIFICATION_MESSAGE_ID; } - - @Override - public Encoded[] getInputs() { - return new Encoded[0]; - } } @@ -250,28 +247,20 @@ public Encoded[] getInputs() { } } - public static class ContactDowngradeNotificationMessage extends ConcreteProtocolMessage { + public static class ContactDowngradeNotificationMessage extends EmptyProtocolMessage { public ContactDowngradeNotificationMessage(CoreProtocolMessage coreProtocolMessage) { super(coreProtocolMessage); } @SuppressWarnings("unused") public ContactDowngradeNotificationMessage(ReceivedMessage receivedMessage) throws Exception { - super(new CoreProtocolMessage(receivedMessage)); - if (receivedMessage.getInputs().length != 0) { - throw new Exception(); - } + super(receivedMessage); } @Override public int getProtocolMessageId() { return CONTACT_DOWNGRADE_NOTIFICATION_MESSAGE_ID; } - - @Override - public Encoded[] getInputs() { - return new Encoded[0]; - } } public static class PropagateContactDowngradeMessage extends ConcreteProtocolMessage { @@ -304,6 +293,23 @@ public Encoded[] getInputs() { } } + + public static class PerformContactDeviceDiscoveryMessage extends EmptyProtocolMessage { + protected PerformContactDeviceDiscoveryMessage(CoreProtocolMessage coreProtocolMessage) { + super(coreProtocolMessage); + } + + @SuppressWarnings("unused") + public PerformContactDeviceDiscoveryMessage(ReceivedMessage receivedMessage) throws Exception { + super(receivedMessage); + } + + @Override + public int getProtocolMessageId() { + return PERFORM_CONTACT_DEVICE_DISCOVERY_MESSAGE_ID; + } + } + // endregion @@ -324,6 +330,7 @@ protected Class[] getPossibleStepClasses(int stateId) { DowngradeContactStep.class, ProcessContactDowngradeNotificationStep.class, ProcessPropagatedContactDowngradeStep.class, + ProcessPerformContactDeviceDiscoveryMessageStep.class, }; } return new Class[0]; @@ -349,9 +356,11 @@ public ConcreteProtocolState executeStep() throws Exception { { int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagateContactDeletionMessage(coreProtocolMessage, receivedMessage.contactIdentity).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateContactDeletionMessage(coreProtocolMessage, receivedMessage.contactIdentity).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } @@ -509,9 +518,11 @@ public ConcreteProtocolState executeStep() throws Exception { // propagate downgrade to other owned devices int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagateContactDowngradeMessage(coreProtocolMessage, receivedMessage.contactIdentity).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateContactDowngradeMessage(coreProtocolMessage, receivedMessage.contactIdentity).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } @@ -569,6 +580,32 @@ public ConcreteProtocolState executeStep() throws Exception { } } + public static class ProcessPerformContactDeviceDiscoveryMessageStep extends ProtocolStep { + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private final InitialProtocolState startState; + private final PerformContactDeviceDiscoveryMessage receivedMessage; + + public ProcessPerformContactDeviceDiscoveryMessageStep(InitialProtocolState startState, PerformContactDeviceDiscoveryMessage receivedMessage, ContactManagementProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createAnyObliviousChannelInfo(), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), + ConcreteProtocol.DEVICE_DISCOVERY_PROTOCOL_ID, + new UID(getPrng()), + false); + ChannelMessageToSend message = new DeviceDiscoveryProtocol.InitialMessage(coreProtocolMessage, receivedMessage.getReceptionChannelInfo().getRemoteIdentity()).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message, getPrng()); + + return new FinalState(); + } + } + // endregion } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/ContactMutualIntroductionProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/ContactMutualIntroductionProtocol.java index 4c5596d2..c96b9ec6 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/ContactMutualIntroductionProtocol.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/ContactMutualIntroductionProtocol.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.HashMap; import java.util.UUID; import io.olvid.engine.Logger; @@ -28,12 +29,14 @@ import io.olvid.engine.crypto.Signature; import io.olvid.engine.datatypes.Constants; import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.datatypes.NoAcceptableChannelException; import io.olvid.engine.datatypes.UID; import io.olvid.engine.datatypes.containers.ChannelMessageToSend; import io.olvid.engine.datatypes.containers.DialogType; import io.olvid.engine.datatypes.containers.ReceptionChannelInfo; import io.olvid.engine.datatypes.containers.SendChannelInfo; import io.olvid.engine.datatypes.containers.TrustOrigin; +import io.olvid.engine.datatypes.notifications.ProtocolNotifications; import io.olvid.engine.encoder.Encoded; import io.olvid.engine.protocol.databases.ReceivedMessage; import io.olvid.engine.protocol.databases.WaitingForOneToOneContactProtocolInstance; @@ -42,6 +45,7 @@ import io.olvid.engine.protocol.protocol_engine.ConcreteProtocol; import io.olvid.engine.protocol.protocol_engine.ConcreteProtocolMessage; import io.olvid.engine.protocol.protocol_engine.ConcreteProtocolState; +import io.olvid.engine.protocol.protocol_engine.EmptyProtocolMessage; import io.olvid.engine.protocol.protocol_engine.InitialProtocolState; import io.olvid.engine.protocol.protocol_engine.OneWayDialogProtocolMessage; import io.olvid.engine.protocol.protocol_engine.ProtocolStep; @@ -300,6 +304,7 @@ public Encoded encode() { private static final int PROPAGATE_NOTIFICATION_MESSAGE_ID = 5; private static final int ACK_MESSAGE_ID = 6; private static final int TRUST_LEVEL_INCREASED_MESSAGE_ID = 7; + private static final int PROPAGATED_INITIAL_MESSAGE = 9; // we skip 8 to stay in sync with iOS @Override protected Class getMessageClass(int protocolMessageId) { @@ -320,6 +325,8 @@ protected Class getMessageClass(int protocolMessageId) { return AckMessage.class; case TRUST_LEVEL_INCREASED_MESSAGE_ID: return TrustLevelIncreasedMessage.class; + case PROPAGATED_INITIAL_MESSAGE: + return PropagatedInitialMessage.class; default: return null; } @@ -327,8 +334,8 @@ protected Class getMessageClass(int protocolMessageId) { public static class InitialMessage extends ConcreteProtocolMessage { - private final Identity contactIdentityA; - private final Identity contactIdentityB; + protected final Identity contactIdentityA; + protected final Identity contactIdentityB; public InitialMessage(CoreProtocolMessage coreProtocolMessage, Identity contactIdentityA, Identity contactIdentityB) { super(coreProtocolMessage); @@ -395,29 +402,6 @@ public Encoded[] getInputs() { } - public static class TrustLevelIncreasedMessage extends ConcreteProtocolMessage { - Identity trustLevelIncreasedIdentity; - - public TrustLevelIncreasedMessage(ReceivedMessage receivedMessage) throws Exception { - super(new CoreProtocolMessage(receivedMessage)); - if (receivedMessage.getInputs().length != 1) { - throw new Exception(); - } - this.trustLevelIncreasedIdentity = receivedMessage.getInputs()[0].decodeIdentity(); - } - - @Override - public int getProtocolMessageId() { - return TRUST_LEVEL_INCREASED_MESSAGE_ID; - } - - @Override - public Encoded[] getInputs() { - return new Encoded[0]; - } - } - - public static class DialogAcceptMediatorInviteMessage extends ConcreteProtocolMessage { private final boolean invitationAccepted; private final UUID dialogUuid; @@ -559,7 +543,7 @@ public Encoded[] getInputs() { } - public static class AckMessage extends ConcreteProtocolMessage { + public static class AckMessage extends EmptyProtocolMessage { AckMessage(CoreProtocolMessage coreProtocolMessage) { super(coreProtocolMessage); @@ -567,15 +551,32 @@ public static class AckMessage extends ConcreteProtocolMessage { @SuppressWarnings("unused") public AckMessage(ReceivedMessage receivedMessage) throws Exception { + super(receivedMessage); + } + + @Override + public int getProtocolMessageId() { + return ACK_MESSAGE_ID; + } + } + + + public static class TrustLevelIncreasedMessage extends ConcreteProtocolMessage { + Identity trustLevelIncreasedIdentity; + + // no other constructor needed here. Instances are created by WaitingForOneToOneContactProtocolInstance.getGenericProtocolMessageToSendWhenTrustLevelIncreased() + + public TrustLevelIncreasedMessage(ReceivedMessage receivedMessage) throws Exception { super(new CoreProtocolMessage(receivedMessage)); - if (receivedMessage.getInputs().length != 0) { + if (receivedMessage.getInputs().length != 1) { throw new Exception(); } + this.trustLevelIncreasedIdentity = receivedMessage.getInputs()[0].decodeIdentity(); } @Override public int getProtocolMessageId() { - return ACK_MESSAGE_ID; + return TRUST_LEVEL_INCREASED_MESSAGE_ID; } @Override @@ -584,6 +585,22 @@ public Encoded[] getInputs() { } } + + public static class PropagatedInitialMessage extends InitialMessage { + public PropagatedInitialMessage(CoreProtocolMessage coreProtocolMessage, Identity contactIdentityA, Identity contactIdentityB) { + super(coreProtocolMessage, contactIdentityA, contactIdentityB); + } + + public PropagatedInitialMessage(ReceivedMessage receivedMessage) throws Exception { + super(receivedMessage); + } + + @Override + public int getProtocolMessageId() { + return PROPAGATED_INITIAL_MESSAGE; + } + } + // endregion @@ -600,7 +617,7 @@ public Encoded[] getInputs() { protected Class[] getPossibleStepClasses(int stateId) { switch (stateId) { case INITIAL_STATE_ID: - return new Class[]{IntroduceContactsStep.class, CheckTrustLevelsAndShowDialogStep.class}; + return new Class[]{IntroduceContactsStep.class, ProcessPropagatedInitialMessageStep.class, CheckTrustLevelsAndShowDialogStep.class}; case INVITATION_RECEIVED_STATE_ID: return new Class[]{PropagateInviteResponseStep.class, ProcessPropagatedInviteResponseStep.class, ReCheckTrustLevelsAfterTrustLevelIncreaseStep.class}; case INVITATION_ACCEPTED_STATE_ID: @@ -656,11 +673,60 @@ public ConcreteProtocolState executeStep() throws Exception { protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); } + { + // if we have other devices, propagate the invite so the invitation sent messages can be inserted in the relevant discussion + int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; + if (numberOfOtherDevices > 0) { + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagatedInitialMessage(coreProtocolMessage, receivedMessage.contactIdentityA, receivedMessage.contactIdentityB).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } + } + } + + // send a notification to insert invitation sent messages in relevant discussions + protocolManagerSession.session.addSessionCommitListener(() -> { + HashMap userInfo = new HashMap<>(); + userInfo.put(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_SENT_OWNED_IDENTITY_KEY, getOwnedIdentity()); + userInfo.put(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_SENT_CONTACT_IDENTITY_A_KEY, receivedMessage.contactIdentityA); + userInfo.put(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_SENT_CONTACT_IDENTITY_B_KEY, receivedMessage.contactIdentityB); + protocolManagerSession.notificationPostingDelegate.postNotification(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_SENT, userInfo); + }); + return new ContactsIntroducedState(); } } + public static class ProcessPropagatedInitialMessageStep extends ProtocolStep { + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private final InitialProtocolState startState; + private final PropagatedInitialMessage receivedMessage; + + public ProcessPropagatedInitialMessageStep(InitialProtocolState startState, PropagatedInitialMessage receivedMessage, ContactMutualIntroductionProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createAnyObliviousChannelWithOwnedDeviceInfo(), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + // send a notification to insert invitation sent messages in relevant discussions + protocolManagerSession.session.addSessionCommitListener(() -> { + HashMap userInfo = new HashMap<>(); + userInfo.put(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_SENT_OWNED_IDENTITY_KEY, getOwnedIdentity()); + userInfo.put(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_SENT_CONTACT_IDENTITY_A_KEY, receivedMessage.contactIdentityA); + userInfo.put(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_SENT_CONTACT_IDENTITY_B_KEY, receivedMessage.contactIdentityB); + protocolManagerSession.notificationPostingDelegate.postNotification(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_SENT, userInfo); + }); + + return new ContactsIntroducedState(); + } + } + public static class CheckTrustLevelsAndShowDialogStep extends ProtocolStep { @SuppressWarnings({"FieldCanBeLocal", "unused"}) private final InitialProtocolState startState; @@ -784,7 +850,6 @@ public ConcreteProtocolState executeStep() throws Exception { { // also insert a WaitingForTrustLevelIncrease to re-evaluate if needed (and delete the previous one) - // one of the 2 creates will fail (duplicate primary key) but this is not a problem WaitingForOneToOneContactProtocolInstance instance = WaitingForOneToOneContactProtocolInstance.get( protocolManagerSession, getProtocolInstanceUid(), @@ -831,12 +896,24 @@ public ConcreteProtocolState executeStep() throws Exception { // Propagate the accept/reject to other owned devices int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagateConfirmationMessage(coreProtocolMessage, receivedMessage.invitationAccepted, startState.contactIdentity, startState.contactSerializedDetails, startState.mediatorIdentity).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateConfirmationMessage(coreProtocolMessage, receivedMessage.invitationAccepted, startState.contactIdentity, startState.contactSerializedDetails, startState.mediatorIdentity).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } + // send a notification to insert invitation accepted/ignored messages in the discussion + protocolManagerSession.session.addSessionCommitListener(() -> { + HashMap userInfo = new HashMap<>(); + userInfo.put(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE_OWNED_IDENTITY_KEY, getOwnedIdentity()); + userInfo.put(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE_MEDIATOR_IDENTITY_KEY, startState.mediatorIdentity); + userInfo.put(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE_CONTACT_SERIALIZED_DETAILS_KEY, startState.contactSerializedDetails); + userInfo.put(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE_ACCEPTED_KEY, receivedMessage.invitationAccepted); + protocolManagerSession.notificationPostingDelegate.postNotification(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE, userInfo); + }); + if (receivedMessage.invitationAccepted) { { // Display invitation accepted dialog @@ -892,17 +969,25 @@ public ProcessPropagatedInviteResponseStep(InvitationReceivedState startState, P public ConcreteProtocolState executeStep() throws Exception { ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); - if (receivedMessage.invitationAccepted) { - UUID dialogUuid = UUID.randomUUID(); + // send a notification to insert invitation accepted/ignored messages in the discussion + protocolManagerSession.session.addSessionCommitListener(() -> { + HashMap userInfo = new HashMap<>(); + userInfo.put(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE_OWNED_IDENTITY_KEY, getOwnedIdentity()); + userInfo.put(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE_MEDIATOR_IDENTITY_KEY, startState.mediatorIdentity); + userInfo.put(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE_CONTACT_SERIALIZED_DETAILS_KEY, startState.contactSerializedDetails); + userInfo.put(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE_ACCEPTED_KEY, receivedMessage.invitationAccepted); + protocolManagerSession.notificationPostingDelegate.postNotification(ProtocolNotifications.NOTIFICATION_CONTACT_INTRODUCTION_INVITATION_RESPONSE, userInfo); + }); + if (receivedMessage.invitationAccepted) { { // Display invitation accepted dialog - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createUserInterfaceChannelInfo(getOwnedIdentity(), DialogType.createMediatorInviteAcceptedDialog(receivedMessage.contactSerializedDetails, receivedMessage.contactIdentity, receivedMessage.mediatorIdentity), dialogUuid)); + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createUserInterfaceChannelInfo(getOwnedIdentity(), DialogType.createMediatorInviteAcceptedDialog(receivedMessage.contactSerializedDetails, receivedMessage.contactIdentity, receivedMessage.mediatorIdentity), startState.dialogUuid)); ChannelMessageToSend messageToSend = new OneWayDialogProtocolMessage(coreProtocolMessage).generateChannelDialogMessageToSend(); protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); } - return new InvitationAcceptedState(receivedMessage.contactIdentity, receivedMessage.contactSerializedDetails, receivedMessage.mediatorIdentity, dialogUuid, ACCEPT_TYPE_MANUAL); + return new InvitationAcceptedState(receivedMessage.contactIdentity, receivedMessage.contactSerializedDetails, receivedMessage.mediatorIdentity, startState.dialogUuid, ACCEPT_TYPE_MANUAL); } else { { // remove the dialog @@ -950,7 +1035,7 @@ public ConcreteProtocolState executeStep() throws Exception { protocolManagerSession.identityDelegate.addTrustOriginToContact(protocolManagerSession.session, startState.contactIdentity, getOwnedIdentity(), TrustOrigin.createIntroductionTrustOrigin(System.currentTimeMillis(), startState.mediatorIdentity), true); } for (UID contactDeviceUid: receivedMessage.contactDeviceUids) { - protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), startState.contactIdentity, contactDeviceUid); + protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), startState.contactIdentity, contactDeviceUid, false); } @@ -958,9 +1043,11 @@ public ConcreteProtocolState executeStep() throws Exception { // Propagate the notification to other owned devices int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagateNotificationMessage(coreProtocolMessage, receivedMessage.contactDeviceUids).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateNotificationMessage(coreProtocolMessage, receivedMessage.contactDeviceUids).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } @@ -997,7 +1084,7 @@ public ConcreteProtocolState executeStep() throws Exception { protocolManagerSession.identityDelegate.addTrustOriginToContact(protocolManagerSession.session, startState.contactIdentity, getOwnedIdentity(), TrustOrigin.createIntroductionTrustOrigin(System.currentTimeMillis(), startState.mediatorIdentity), true); } for (UID contactDeviceUid: receivedMessage.contactDeviceUids) { - protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), startState.contactIdentity, contactDeviceUid); + protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), startState.contactIdentity, contactDeviceUid, false); } return new WaitingForAckState(startState.contactIdentity, startState.contactSerializedDetails, startState.mediatorIdentity, startState.dialogUuid, startState.acceptType); diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/DeviceCapabilitiesDiscoveryProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/DeviceCapabilitiesDiscoveryProtocol.java index c23777ad..d0a708c1 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/DeviceCapabilitiesDiscoveryProtocol.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/DeviceCapabilitiesDiscoveryProtocol.java @@ -29,6 +29,7 @@ import io.olvid.engine.Logger; import io.olvid.engine.crypto.PRNGService; import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.datatypes.NoAcceptableChannelException; import io.olvid.engine.datatypes.UID; import io.olvid.engine.datatypes.containers.ChannelMessageToSend; import io.olvid.engine.datatypes.containers.ReceptionChannelInfo; @@ -421,9 +422,11 @@ public ConcreteProtocolState executeStep() throws Exception { int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new OwnCapabilitiesToSelfMessage(coreProtocolMessage, ObvCapability.capabilityListToStringArray(receivedMessage.newOwnCapabilities), false).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new OwnCapabilitiesToSelfMessage(coreProtocolMessage, ObvCapability.capabilityListToStringArray(receivedMessage.newOwnCapabilities), false).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/DeviceDiscoveryProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/DeviceDiscoveryProtocol.java index 6f8280e2..630b60f1 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/DeviceDiscoveryProtocol.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/DeviceDiscoveryProtocol.java @@ -23,9 +23,11 @@ import java.util.Arrays; import java.util.HashSet; +import java.util.Objects; import io.olvid.engine.Logger; import io.olvid.engine.crypto.PRNGService; +import io.olvid.engine.datatypes.Constants; import io.olvid.engine.datatypes.Identity; import io.olvid.engine.datatypes.UID; import io.olvid.engine.datatypes.containers.ChannelMessageToSend; @@ -81,7 +83,7 @@ protected Class getStateClass(int stateId) { } public static class WaitingForChildProtocolState extends ConcreteProtocolState { - private final Identity contactIdentity; // The contact uid we seek to trust + private final Identity contactIdentity; public WaitingForChildProtocolState(Encoded encodedState) throws Exception { super(WAITING_FOR_CHILD_PROTOCOL_STATE_ID); @@ -313,12 +315,13 @@ public ConcreteProtocolState executeStep() throws Exception { return new CancelledState(); } - HashSet newContactDeviceUids = new HashSet<>(Arrays.asList(deviceUidsReceivedState.getDeviceUids())); - if (newContactDeviceUids.isEmpty()) { - Logger.w("Device discovery did not find any device! Probably an expired query."); + if (deviceUidsReceivedState.getDeviceUids().length == 1 + && Objects.equals(deviceUidsReceivedState.getDeviceUids()[0], Constants.BROADCAST_UID)) { + Logger.w("Device discovery query expired."); return new CancelledState(); } + HashSet newContactDeviceUids = new HashSet<>(Arrays.asList(deviceUidsReceivedState.getDeviceUids())); HashSet oldContactDeviceUids = new HashSet<>(Arrays.asList(protocolManagerSession.identityDelegate.getDeviceUidsOfContactIdentity(protocolManagerSession.session, getOwnedIdentity(), receivedContactIdentity))); for (UID contactDeviceUid: oldContactDeviceUids) { if (!newContactDeviceUids.contains(contactDeviceUid)) { @@ -330,7 +333,7 @@ public ConcreteProtocolState executeStep() throws Exception { for (UID contactDeviceUid: newContactDeviceUids) { if (!oldContactDeviceUids.contains(contactDeviceUid)) { // a new deviceUid was found --> add it, this will trigger the channel creation - protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), receivedContactIdentity, contactDeviceUid); + protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), receivedContactIdentity, contactDeviceUid, false); } } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/GroupInvitationProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/GroupInvitationProtocol.java index c945fac6..a8cb91ba 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/GroupInvitationProtocol.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/GroupInvitationProtocol.java @@ -28,6 +28,7 @@ import io.olvid.engine.crypto.PRNGService; import io.olvid.engine.datatypes.GroupMembersChangedCallback; import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.datatypes.NoAcceptableChannelException; import io.olvid.engine.datatypes.UID; import io.olvid.engine.datatypes.containers.ChannelMessageToSend; import io.olvid.engine.datatypes.containers.DialogType; @@ -523,9 +524,11 @@ public ConcreteProtocolState executeStep() throws Exception { // Propagate the accept to other owned devices int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagateInvitationResponseMessage(coreProtocolMessage, true).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateInvitationResponseMessage(coreProtocolMessage, true).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } { @@ -596,9 +599,11 @@ public ConcreteProtocolState executeStep() throws Exception { // Propagate the accept to other owned devices int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagateInvitationResponseMessage(coreProtocolMessage, invitationAccepted).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateInvitationResponseMessage(coreProtocolMessage, invitationAccepted).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } @@ -640,7 +645,8 @@ public ConcreteProtocolState executeStep() throws Exception { getOwnedIdentity(), startState.groupInformation, new Identity[]{groupOwnerIdentity}, - pendingGroupMembers + pendingGroupMembers, + false ); } @@ -701,7 +707,8 @@ public ConcreteProtocolState executeStep() throws Exception { getOwnedIdentity(), startState.groupInformation, new Identity[]{groupOwnerIdentity}, - pendingGroupMembers + pendingGroupMembers, + false ); } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/GroupManagementProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/GroupManagementProtocol.java index 07913084..0acc3f8a 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/GroupManagementProtocol.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/GroupManagementProtocol.java @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.HashSet; +import java.util.Objects; import io.olvid.engine.Logger; import io.olvid.engine.crypto.AuthEnc; @@ -30,6 +31,7 @@ import io.olvid.engine.crypto.Suite; import io.olvid.engine.datatypes.GroupMembersChangedCallback; import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.datatypes.NoAcceptableChannelException; import io.olvid.engine.datatypes.UID; import io.olvid.engine.datatypes.containers.ChannelMessageToSend; import io.olvid.engine.datatypes.containers.Group; @@ -130,6 +132,9 @@ public Encoded encode() { private static final int TRIGGER_REINVITE_MESSAGE_ID = 13; private static final int TRIGGER_UPDATE_MEMBERS_MESSAGE_ID = 14; private static final int UPLOAD_GROUP_PHOTO_MESSAGE_MESSAGE_ID = 15; + private static final int PROPAGATE_REINVITE_PENDING_MEMBER_MESSAGE_ID = 16; + private static final int PROPAGATE_DISBAND_GROUP_MESSAGE_ID = 17; + private static final int PROPAGATE_LEAVE_GROUP_MESSAGE_ID = 18; @Override protected Class getMessageClass(int protocolMessageId) { @@ -166,6 +171,12 @@ protected Class getMessageClass(int protocolMessageId) { return TriggerUpdateMembersMessage.class; case UPLOAD_GROUP_PHOTO_MESSAGE_MESSAGE_ID: return UploadGroupPhotoMessage.class; + case PROPAGATE_REINVITE_PENDING_MEMBER_MESSAGE_ID: + return PropagateReinvitePendingMemberMessage.class; + case PROPAGATE_DISBAND_GROUP_MESSAGE_ID: + return PropagateDisbandGroupMessage.class; + case PROPAGATE_LEAVE_GROUP_MESSAGE_ID: + return PropagateLeaveGroupMessage.class; default: return null; } @@ -447,8 +458,8 @@ public Encoded[] getInputs() { } public static class ReinvitePendingMemberMessage extends ConcreteProtocolMessage { - private final GroupInformation groupInformation; - private final Identity pendingMemberIdentity; + protected final GroupInformation groupInformation; + protected final Identity pendingMemberIdentity; public ReinvitePendingMemberMessage(CoreProtocolMessage coreProtocolMessage, GroupInformation groupInformation, Identity pendingMemberIdentity) { super(coreProtocolMessage); @@ -679,6 +690,57 @@ public Encoded[] getInputs() { }; } } + + + public static class PropagateReinvitePendingMemberMessage extends ReinvitePendingMemberMessage { + public PropagateReinvitePendingMemberMessage(CoreProtocolMessage coreProtocolMessage, GroupInformation groupInformation, Identity pendingMemberIdentity) { + super(coreProtocolMessage, groupInformation, pendingMemberIdentity); + } + + @SuppressWarnings("unused") + public PropagateReinvitePendingMemberMessage(ReceivedMessage receivedMessage) throws Exception { + super(receivedMessage); + } + + @Override + public int getProtocolMessageId() { + return PROPAGATE_REINVITE_PENDING_MEMBER_MESSAGE_ID; + } + } + + public static class PropagateDisbandGroupMessage extends GroupInformationOnlyMessage { + public PropagateDisbandGroupMessage(CoreProtocolMessage coreProtocolMessage, GroupInformation groupInformation) { + super(coreProtocolMessage, groupInformation); + } + + @SuppressWarnings("unused") + public PropagateDisbandGroupMessage(ReceivedMessage receivedMessage) throws Exception { + super(receivedMessage); + } + + @Override + public int getProtocolMessageId() { + return PROPAGATE_DISBAND_GROUP_MESSAGE_ID; + } + } + + public static class PropagateLeaveGroupMessage extends GroupInformationOnlyMessage { + public PropagateLeaveGroupMessage(CoreProtocolMessage coreProtocolMessage, GroupInformation groupInformation) { + super(coreProtocolMessage, groupInformation); + } + + @SuppressWarnings("unused") + public PropagateLeaveGroupMessage(ReceivedMessage receivedMessage) throws Exception { + super(receivedMessage); + } + + @Override + public int getProtocolMessageId() { + return PROPAGATE_LEAVE_GROUP_MESSAGE_ID; + } + } + + // endregion @@ -690,14 +752,18 @@ protected Class[] getPossibleStepClasses(int stateId) { if (stateId == INITIAL_STATE_ID) { return new Class[]{ InitiateGroupCreationStep.class, + ProcessPropagateGroupCreationMessage.class, NotifyMembersChangedStep.class, ProcessNewMembersStep.class, AddGroupMembersStep.class, RemoveGroupMembersStep.class, GetKickedStep.class, ReinvitePendingMemberStep.class, + ProcessPropagateReinvitePendingMemberStep.class, DisbandGroupStep.class, + ProcessPropagateDisbandGroupMessageStep.class, LeaveGroupStep.class, + ProcessPropagateLeaveGroupMessageStep.class, ProcessGroupLeftStep.class, QueryGroupMembersStep.class, SendGroupMembersStep.class, @@ -747,7 +813,8 @@ public ConcreteProtocolState executeStep() throws Exception { getOwnedIdentity(), groupInformation, new Identity[0], - receivedMessage.groupMemberIdentitiesAndSerializedDetails.toArray(new IdentityWithSerializedDetails[0]) + receivedMessage.groupMemberIdentitiesAndSerializedDetails.toArray(new IdentityWithSerializedDetails[0]), + false ); if (receivedMessage.absolutePhotoUrl != null && receivedMessage.absolutePhotoUrl.length() > 0) { @@ -783,9 +850,11 @@ public ConcreteProtocolState executeStep() throws Exception { // Propagate the group creation to other owned devices int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagateGroupCreationMessage(coreProtocolMessage, groupInformation, receivedMessage.groupMemberIdentitiesAndSerializedDetails).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateGroupCreationMessage(coreProtocolMessage, groupInformation, receivedMessage.groupMemberIdentitiesAndSerializedDetails).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } @@ -809,6 +878,70 @@ public ConcreteProtocolState executeStep() throws Exception { } } + + public static class ProcessPropagateGroupCreationMessage extends ProtocolStep { + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private final InitialProtocolState startState; + private final PropagateGroupCreationMessage receivedMessage; + + public ProcessPropagateGroupCreationMessage(InitialProtocolState startState, PropagateGroupCreationMessage receivedMessage, GroupManagementProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createAnyObliviousChannelWithOwnedDeviceInfo(), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + if (receivedMessage.groupMemberIdentitiesAndSerializedDetails.contains(new IdentityWithSerializedDetails(getOwnedIdentity(), ""))) { + Logger.w("Error: the groupMemberIdentitiesAndSerializedDetails contains the ownedIdentity"); + return null; + } + + if (!receivedMessage.groupInformation.groupOwnerIdentity.equals(getOwnedIdentity())) { + Logger.w("Error: the groupInformation contains a different Identity than ownedIdentity"); + return null; + } + + if (!receivedMessage.groupInformation.computeProtocolUid().equals(protocol.getProtocolInstanceUid())) { + Logger.w("Error: protocolUid mismatch"); + return null; + } + + // Create the ContactGroup in database + protocolManagerSession.identityDelegate.createContactGroup( + protocolManagerSession.session, + getOwnedIdentity(), + receivedMessage.groupInformation, + new Identity[0], + receivedMessage.groupMemberIdentitiesAndSerializedDetails.toArray(new IdentityWithSerializedDetails[0]), + true + ); + + + // check if a group photo needs to be downloaded + JsonGroupDetailsWithVersionAndPhoto jsonGroupDetailsWithVersionAndPhoto = protocol.getJsonObjectMapper().readValue(receivedMessage.groupInformation.serializedGroupDetailsWithVersionAndPhoto, JsonGroupDetailsWithVersionAndPhoto.class); + + if (jsonGroupDetailsWithVersionAndPhoto.getPhotoServerLabel() != null && jsonGroupDetailsWithVersionAndPhoto.getPhotoServerKey() != null) { + // even though another device created the group, we create a ServerUserData to ensure this photo is retained on server + protocolManagerSession.identityDelegate.createGroupV1ServerUserData(protocolManagerSession.session, getOwnedIdentity(), new UID(jsonGroupDetailsWithVersionAndPhoto.getPhotoServerLabel()), receivedMessage.groupInformation.getGroupOwnerAndUid()); + + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage( + SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), + DOWNLOAD_GROUP_PHOTO_CHILD_PROTOCOL_ID, + new UID(getPrng()), + false + ); + ChannelMessageToSend messageToSend = new DownloadGroupPhotoChildProtocol.InitialMessage(coreProtocolMessage, receivedMessage.groupInformation).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } + + return new FinalState(); + } + } + + public static class NotifyMembersChangedStep extends ProtocolStep { @SuppressWarnings("unused") private final InitialProtocolState startState; @@ -881,10 +1014,20 @@ public ConcreteProtocolState executeStep() throws Exception { // also add yourself (group owner) to the group groupMembers.add(new IdentityWithSerializedDetails(getOwnedIdentity(), protocolManagerSession.identityDelegate.getSerializedPublishedDetailsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()))); + // compute the identities to which the message should be sent (include myself in multi-device) + final Identity[] recipientIdentities; + if (protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length > 0) { + recipientIdentities = new Identity[group.getGroupMembers().length + 1]; + recipientIdentities[0] = getOwnedIdentity(); + System.arraycopy(group.getGroupMembers(), 0, recipientIdentities, 1, group.getGroupMembers().length); + } else { + recipientIdentities = group.getGroupMembers(); + } + { - if (group.getGroupMembers().length > 0) { + if (recipientIdentities.length > 0) { // notify all group members (not the pending group members) with a single message - SendChannelInfo[] sendChannelInfos = SendChannelInfo.createAllConfirmedObliviousChannelsInfosForMultipleIdentities(group.getGroupMembers(), getOwnedIdentity()); + SendChannelInfo[] sendChannelInfos = SendChannelInfo.createAllConfirmedObliviousChannelsInfosForMultipleIdentities(recipientIdentities, getOwnedIdentity()); for (SendChannelInfo sendChannelInfo : sendChannelInfos) { try { CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(sendChannelInfo); @@ -924,6 +1067,8 @@ public ConcreteProtocolState executeStep() throws Exception { return null; } + boolean receivedFromOtherOwnedDevice = Objects.equals(receivedMessage.getReceptionChannelInfo().getRemoteIdentity(), getOwnedIdentity()); + // get the group Group group = protocolManagerSession.identityDelegate.getGroup(protocolManagerSession.session, getOwnedIdentity(), receivedMessage.groupInformation.getGroupOwnerAndUid()); @@ -931,8 +1076,11 @@ public ConcreteProtocolState executeStep() throws Exception { return null; } - if (group.getGroupOwner() == null) { - Logger.w("Error: received a NewMembersMessage for a group you own"); + if (!receivedFromOtherOwnedDevice && group.getGroupOwner() == null) { + Logger.w("Error: received a NewMembersMessage from someone else for a group you own"); + return null; + } else if (receivedFromOtherOwnedDevice && group.getGroupOwner() != null) { + Logger.w("Error: received a NewMembersMessage from another owned device and for a group you do not own"); return null; } @@ -953,8 +1101,14 @@ public ConcreteProtocolState executeStep() throws Exception { ((jsonGroupDetailsWithVersionAndPhoto.getPhotoServerKey() == null && publishedDetails.getPhotoServerKey() == null) || (jsonGroupDetailsWithVersionAndPhoto.getPhotoServerKey() != null && publishedDetails.getPhotoServerKey() != null && new Encoded(jsonGroupDetailsWithVersionAndPhoto.getPhotoServerKey()).decodeSymmetricKey().equals(new Encoded(publishedDetails.getPhotoServerKey()).decodeSymmetricKey()))) && publishedDetails.getPhotoUrl() != null)) { - // we need to download the photo, so we start a child protocol + // we need to download the photo + + // if we are the owner, create a server user data + if (receivedFromOtherOwnedDevice) { + protocolManagerSession.identityDelegate.createGroupV1ServerUserData(protocolManagerSession.session, getOwnedIdentity(), new UID(jsonGroupDetailsWithVersionAndPhoto.getPhotoServerLabel()), receivedMessage.groupInformation.getGroupOwnerAndUid()); + } + // we start a child protocol CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage( SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), DOWNLOAD_GROUP_PHOTO_CHILD_PROTOCOL_ID, @@ -1194,10 +1348,59 @@ public ConcreteProtocolState executeStep() throws Exception { protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); } + + { + // Propagate the group re-invite to other owned devices + int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; + if (numberOfOtherDevices > 0) { + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateReinvitePendingMemberMessage(coreProtocolMessage, receivedMessage.groupInformation, receivedMessage.pendingMemberIdentity).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } + } + } + return new FinalState(); } } + + public static class ProcessPropagateReinvitePendingMemberStep extends ProtocolStep { + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private final InitialProtocolState startState; + private final PropagateReinvitePendingMemberMessage receivedMessage; + + public ProcessPropagateReinvitePendingMemberStep(InitialProtocolState startState, PropagateReinvitePendingMemberMessage receivedMessage, GroupManagementProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createAnyObliviousChannelWithOwnedDeviceInfo(), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + final ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + if (!receivedMessage.groupInformation.groupOwnerIdentity.equals(getOwnedIdentity())) { + Logger.w("Error: the groupInformation contains a different Identity than ownedIdentity"); + return null; + } + + if (!receivedMessage.groupInformation.computeProtocolUid().equals(protocol.getProtocolInstanceUid())) { + Logger.w("Error: protocolUid mismatch"); + return null; + } + + { + // mark the pending member as "not declined" + protocolManagerSession.identityDelegate.setPendingMemberDeclined(protocolManagerSession.session, receivedMessage.groupInformation.getGroupOwnerAndUid(), getOwnedIdentity(), receivedMessage.pendingMemberIdentity, false); + } + + return new FinalState(); + } + } + + public static class DisbandGroupStep extends ProtocolStep { @SuppressWarnings({"FieldCanBeLocal", "unused"}) private final InitialProtocolState startState; @@ -1263,6 +1466,52 @@ public ConcreteProtocolState executeStep() throws Exception { } } + { + // Propagate the disband to other owned devices + int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; + if (numberOfOtherDevices > 0) { + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateDisbandGroupMessage(coreProtocolMessage, receivedMessage.groupInformation).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } + } + } + + { + // delete the group + protocolManagerSession.identityDelegate.deleteGroup(protocolManagerSession.session, receivedMessage.groupInformation.getGroupOwnerAndUid(), getOwnedIdentity()); + } + + return new FinalState(); + } + } + + public static class ProcessPropagateDisbandGroupMessageStep extends ProtocolStep { + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private final InitialProtocolState startState; + private final PropagateDisbandGroupMessage receivedMessage; + + public ProcessPropagateDisbandGroupMessageStep(InitialProtocolState startState, PropagateDisbandGroupMessage receivedMessage, GroupManagementProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createAnyObliviousChannelWithOwnedDeviceInfo(), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + final ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + if (!receivedMessage.groupInformation.groupOwnerIdentity.equals(getOwnedIdentity())) { + Logger.w("Error: the groupInformation contains a different Identity than ownedIdentity"); + return null; + } + + if (!receivedMessage.groupInformation.computeProtocolUid().equals(protocol.getProtocolInstanceUid())) { + Logger.w("Error: protocolUid mismatch"); + return null; + } + { // delete the group protocolManagerSession.identityDelegate.deleteGroup(protocolManagerSession.session, receivedMessage.groupInformation.getGroupOwnerAndUid(), getOwnedIdentity()); @@ -1306,10 +1555,57 @@ public ConcreteProtocolState executeStep() throws Exception { Logger.w("LeaveGroupStep: Error notifying group owner. Probably no channel with him."); } + { + // Propagate the disband to other owned devices + int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; + if (numberOfOtherDevices > 0) { + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateLeaveGroupMessage(coreProtocolMessage, receivedMessage.groupInformation).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } + } + } + + { + // simply delete the group on the engine side, everything will follow! + protocolManagerSession.identityDelegate.leaveGroup(protocolManagerSession.session, receivedMessage.groupInformation.getGroupOwnerAndUid(), getOwnedIdentity()); + } + + return new FinalState(); + } + } + + public static class ProcessPropagateLeaveGroupMessageStep extends ProtocolStep { + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private final InitialProtocolState startState; + private final PropagateLeaveGroupMessage receivedMessage; + + public ProcessPropagateLeaveGroupMessageStep(InitialProtocolState startState, PropagateLeaveGroupMessage receivedMessage, GroupManagementProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createAnyObliviousChannelWithOwnedDeviceInfo(), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + final ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + if (receivedMessage.groupInformation.groupOwnerIdentity.equals(getOwnedIdentity())) { + Logger.w("Error: cannot leave a group you own"); + return null; + } + + if (!receivedMessage.groupInformation.computeProtocolUid().equals(protocol.getProtocolInstanceUid())) { + Logger.w("Error: protocolUid mismatch"); + return null; + } + { // simply delete the group on the engine side, everything will follow! protocolManagerSession.identityDelegate.leaveGroup(protocolManagerSession.session, receivedMessage.groupInformation.getGroupOwnerAndUid(), getOwnedIdentity()); } + return new FinalState(); } } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/GroupsV2Protocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/GroupsV2Protocol.java index 2d84132f..9e44938c 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/GroupsV2Protocol.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/GroupsV2Protocol.java @@ -44,6 +44,7 @@ import io.olvid.engine.datatypes.Constants; import io.olvid.engine.datatypes.EncryptedBytes; import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.datatypes.NoAcceptableChannelException; import io.olvid.engine.datatypes.Seed; import io.olvid.engine.datatypes.UID; import io.olvid.engine.datatypes.containers.ChannelMessageToSend; @@ -375,7 +376,7 @@ public static Encoded encodeMembersToKick(HashMap membersToKic int i = 0; for (Map.Entry entry : membersToKick.entrySet()) { encodeds[i] = Encoded.of(entry.getKey()); - encodeds[i+1] = Encoded.of(entry.getValue()); + encodeds[i + 1] = Encoded.of(entry.getValue()); i += 2; } return Encoded.of(encodeds); @@ -385,7 +386,7 @@ public static HashMap decodeMembersToKick(Encoded encoded) thr HashMap membersToKick = new HashMap<>(); Encoded[] encodeds = encoded.decodeList(); for (int i = 0; i < encodeds.length; i += 2) { - membersToKick.put(encodeds[i].decodeIdentity(), encodeds[i+1].decodeBytes()); + membersToKick.put(encodeds[i].decodeIdentity(), encodeds[i + 1].decodeBytes()); } return membersToKick; } @@ -535,11 +536,6 @@ public Encoded encode() { // endregion - - - - - // region Messages private static final int GROUP_CREATION_INITIAL_MESSAGE_ID = 0; @@ -648,34 +644,52 @@ public static class GroupCreationInitialMessage extends ConcreteProtocolMessage private final HashSet otherGroupMembers; // does not include the group creator identity private final String serializedGroupDetails; // serialized JsonGroupDetails private final String absolutePhotoUrl; + private final String serializedGroupType; // serialized JsonGroupType, may be NULL - public GroupCreationInitialMessage(CoreProtocolMessage coreProtocolMessage, HashSet ownPermissions, HashSet otherGroupMembers, String serializedGroupDetails, String absolutePhotoUrl) { + public GroupCreationInitialMessage(CoreProtocolMessage coreProtocolMessage, HashSet ownPermissions, HashSet otherGroupMembers, String serializedGroupDetails, String absolutePhotoUrl, String serializedGroupType) { super(coreProtocolMessage); this.ownPermissions = ownPermissions; this.otherGroupMembers = otherGroupMembers; this.serializedGroupDetails = serializedGroupDetails; this.absolutePhotoUrl = absolutePhotoUrl; + this.serializedGroupType = serializedGroupType; } @SuppressWarnings("unused") public GroupCreationInitialMessage(ReceivedMessage receivedMessage) throws Exception { super(new CoreProtocolMessage(receivedMessage)); Encoded[] inputs = receivedMessage.getInputs(); - if (inputs.length != 4) { - throw new Exception(); - } - this.ownPermissions = GroupV2.Permission.deserializeKnownPermissions(inputs[0].decodeBytes()); - this.otherGroupMembers = new HashSet<>(); - for (Encoded encodedGroupMember : inputs[1].decodeList()) { - this.otherGroupMembers.add(GroupV2.IdentityAndPermissions.of(encodedGroupMember)); - } - this.serializedGroupDetails = inputs[2].decodeString(); - String url = inputs[3].decodeString(); - if (url.length() == 0) { - this.absolutePhotoUrl = null; + if (inputs.length == 5) { + this.ownPermissions = GroupV2.Permission.deserializeKnownPermissions(inputs[0].decodeBytes()); + this.otherGroupMembers = new HashSet<>(); + for (Encoded encodedGroupMember : inputs[1].decodeList()) { + this.otherGroupMembers.add(GroupV2.IdentityAndPermissions.of(encodedGroupMember)); + } + this.serializedGroupDetails = inputs[2].decodeString(); + String url = inputs[3].decodeString(); + if (url.length() == 0) { + this.absolutePhotoUrl = null; + } else { + this.absolutePhotoUrl = url; + } + this.serializedGroupType = inputs[4].decodeString(); + } else if (inputs.length == 4) { // null serializedGroupType + this.ownPermissions = GroupV2.Permission.deserializeKnownPermissions(inputs[0].decodeBytes()); + this.otherGroupMembers = new HashSet<>(); + for (Encoded encodedGroupMember : inputs[1].decodeList()) { + this.otherGroupMembers.add(GroupV2.IdentityAndPermissions.of(encodedGroupMember)); + } + this.serializedGroupDetails = inputs[2].decodeString(); + String url = inputs[3].decodeString(); + if (url.length() == 0) { + this.absolutePhotoUrl = null; + } else { + this.absolutePhotoUrl = url; + } + this.serializedGroupType = null; } else { - this.absolutePhotoUrl = url; + throw new Exception(); } } @@ -690,13 +704,24 @@ public Encoded[] getInputs() { for (GroupV2.IdentityAndPermissions groupMember : otherGroupMembers) { encodedGroupMembers.add(groupMember.encode()); } - //noinspection ConstantConditions - return new Encoded[] { - Encoded.of(GroupV2.Permission.serializePermissions(ownPermissions)), - Encoded.of(encodedGroupMembers.toArray(new Encoded[0])), - Encoded.of(serializedGroupDetails), - Encoded.of(absolutePhotoUrl == null ? "" : absolutePhotoUrl), - }; + if (serializedGroupType == null) { + //noinspection ConstantConditions + return new Encoded[]{ + Encoded.of(GroupV2.Permission.serializePermissions(ownPermissions)), + Encoded.of(encodedGroupMembers.toArray(new Encoded[0])), + Encoded.of(serializedGroupDetails), + Encoded.of(absolutePhotoUrl == null ? "" : absolutePhotoUrl), + }; + } else { + //noinspection ConstantConditions + return new Encoded[]{ + Encoded.of(GroupV2.Permission.serializePermissions(ownPermissions)), + Encoded.of(encodedGroupMembers.toArray(new Encoded[0])), + Encoded.of(serializedGroupDetails), + Encoded.of(absolutePhotoUrl == null ? "" : absolutePhotoUrl), + Encoded.of(serializedGroupType), + }; + } } } @@ -1161,7 +1186,7 @@ public int getProtocolMessageId() { } - public static class PropagateInvitationDialogResponseMessage extends ConcreteProtocolMessage { + public static class PropagateInvitationDialogResponseMessage extends ConcreteProtocolMessage { private final boolean invitationAccepted; private final byte[] ownGroupInvitationNonce; @@ -1292,7 +1317,7 @@ public int getProtocolMessageId() { @Override public Encoded[] getInputs() { - return new Encoded[] { + return new Encoded[]{ groupIdentifier.encode(), changeSet.encode(), }; @@ -1342,7 +1367,7 @@ public int getProtocolMessageId() { @Override public Encoded[] getInputs() { - return new Encoded[] { + return new Encoded[]{ groupIdentifier.encode(), }; } @@ -1378,7 +1403,7 @@ public int getProtocolMessageId() { @Override public Encoded[] getInputs() { - return new Encoded[] { + return new Encoded[]{ groupIdentifier.encode(), Encoded.of(ownInvitationNonce), }; @@ -1413,7 +1438,7 @@ public int getProtocolMessageId() { @Override public Encoded[] getInputs() { - return new Encoded[] { + return new Encoded[]{ groupIdentifier.encode(), }; } @@ -1445,7 +1470,7 @@ public int getProtocolMessageId() { @Override public Encoded[] getInputs() { - return new Encoded[] { + return new Encoded[]{ groupIdentifier.encode(), }; } @@ -1477,14 +1502,13 @@ public int getProtocolMessageId() { @Override public Encoded[] getInputs() { - return new Encoded[] { + return new Encoded[]{ groupIdentifier.encode(), }; } } - public static class InitiateBatchKeysResendMessage extends ConcreteProtocolMessage { private final Identity contactIdentity; private final UID contactDeviceUid; @@ -1514,7 +1538,7 @@ public int getProtocolMessageId() { @Override public Encoded[] getInputs() { - return new Encoded[] { + return new Encoded[]{ Encoded.of(contactIdentity), Encoded.of(contactDeviceUid), }; @@ -1539,7 +1563,7 @@ public BlobKeysBatchAfterChannelCreationMessage(ReceivedMessage receivedMessage) } Encoded[] encodeds = inputs[0].decodeList(); this.groupInfos = new GroupV2.IdentifierVersionAndKeys[encodeds.length]; - for (int i=0; i< encodeds.length; i++) { + for (int i = 0; i < encodeds.length; i++) { this.groupInfos[i] = new GroupV2.IdentifierVersionAndKeys(encodeds[i]); } } @@ -1552,10 +1576,10 @@ public int getProtocolMessageId() { @Override public Encoded[] getInputs() { Encoded[] encodeds = new Encoded[groupInfos.length]; - for (int i=0; i< groupInfos.length; i++) { + for (int i = 0; i < groupInfos.length; i++) { encodeds[i] = groupInfos[i].encode(); } - return new Encoded[] { + return new Encoded[]{ Encoded.of(encodeds), }; } @@ -1670,7 +1694,7 @@ public int getProtocolMessageId() { @Override public Encoded[] getInputs() { - return new Encoded[] { + return new Encoded[]{ groupIdentifier.encode(), Encoded.of(pendingMemberIdentity), }; @@ -1681,19 +1705,13 @@ public Encoded[] getInputs() { // endregion - - - - - - // region Steps @Override protected Class[] getPossibleStepClasses(int stateId) { switch (stateId) { case INITIAL_STATE_ID: - return new Class[]{InitiateGroupCreationStep.class, ProcessInvitationOrMembersUpdateStep.class, DoNothingAfterServerQueryStep.class, ProcessPingStep.class, InitiateBlobReDownloadStep.class, InitiateGroupUpdateStep.class, GetKickedStep.class, LeaveGroupStep.class, DisbandGroupStep.class, PrepareBatchKeysMessageStep.class, ProcessBatchKeysMessageStep.class, ProcessCreateOrUpdateKeycloakGroupMessage.class, SendKeycloakGroupTargetedPingStep.class }; + return new Class[]{InitiateGroupCreationStep.class, ProcessInvitationOrMembersUpdateStep.class, DoNothingAfterServerQueryStep.class, ProcessPingStep.class, InitiateBlobReDownloadStep.class, InitiateGroupUpdateStep.class, GetKickedStep.class, LeaveGroupStep.class, DisbandGroupStep.class, PrepareBatchKeysMessageStep.class, ProcessBatchKeysMessageStep.class, ProcessCreateOrUpdateKeycloakGroupMessage.class, SendKeycloakGroupTargetedPingStep.class}; case UPLOADING_CREATED_GROUP_DATA_STATE_ID: return new Class[]{CheckIfGroupCreationCanBeFinalizedStep.class, FinalizeGroupCreationStep.class}; case DOWNLOADING_GROUP_BLOB_STATE_ID: @@ -1801,7 +1819,8 @@ public ConcreteProtocolState executeStep() throws Exception { (ServerAuthenticationPrivateKey) groupAdminServerAuthenticationKeyPair.getPrivateKey()), ownGroupInvitationNonce, ownPermissionStrings, - otherGroupMembers + otherGroupMembers, + receivedMessage.serializedGroupType ); } @@ -1904,7 +1923,7 @@ public ConcreteProtocolState executeStep() throws Exception { boolean waitingForBlobUpload = startState.waitingForBlobUpload; boolean waitingForPhotoUpload = startState.waitingForPhotoUpload; - switch (uploadType){ + switch (uploadType) { case BLOB: { if (uploadResult == 0) { waitingForBlobUpload = false; @@ -1950,11 +1969,11 @@ public ConcreteProtocolState executeStep() throws Exception { { - // for each group member & pending member, send - // - for new members the main seed - // - the version seed for everyone + // for each group member send + // - the main seed + // - the version seed // - for admins the groupAdmin private key - // send the message through oblivious channel when possible (required for new members), asymmetric broadcast otherwise + // send the message through oblivious channel GroupV2.BlobKeys blobKeys = protocolManagerSession.identityDelegate.getGroupV2BlobKeys(protocolManagerSession.session, getOwnedIdentity(), startState.groupIdentifier); HashSet groupMembersAndPermissions = protocolManagerSession.identityDelegate.getGroupV2OtherMembersAndPermissions(protocolManagerSession.session, getOwnedIdentity(), startState.groupIdentifier); @@ -1966,6 +1985,7 @@ public ConcreteProtocolState executeStep() throws Exception { UID invitationProtocolInstanceUid = startState.groupIdentifier.computeProtocolInstanceUid(); + // here we loop on OTHER group members, not ourself for (GroupV2.IdentityAndPermissions groupMembersAndPermission : groupMembersAndPermissions) { UID[] contactDeviceUidsWithChannel = protocolManagerSession.channelDelegate.getConfirmedObliviousChannelDeviceUids(protocolManagerSession.session, getOwnedIdentity(), groupMembersAndPermission.identity); if (contactDeviceUidsWithChannel.length > 0) { @@ -1998,6 +2018,27 @@ public ConcreteProtocolState executeStep() throws Exception { return new FinalState(); } } + + // also notify other owned devices + UID[] allOwnedDeviceUids = protocolManagerSession.identityDelegate.getDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); + if (allOwnedDeviceUids.length > 1) { + try { + GroupV2.BlobKeys keysToSend = new GroupV2.BlobKeys( + blobKeys.blobMainSeed, + blobKeys.blobVersionSeed, + blobKeys.groupAdminServerAuthenticationPrivateKey + ); + + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage( + SendChannelInfo.createAllConfirmedObliviousChannelsInfo(getOwnedIdentity(), getOwnedIdentity()), + getProtocolId(), + invitationProtocolInstanceUid, + false); + // we send the full set of owned devices, not only "other" own device uid, so that receiving devices know if all devices were notified + ChannelMessageToSend messageToSend = new InvitationOrMembersUpdateMessage(coreProtocolMessage, startState.groupIdentifier, startState.groupVersion, keysToSend, allOwnedDeviceUids).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } + } } { @@ -2093,8 +2134,6 @@ public ProcessInvitationOrMembersUpdateStep(InitialProtocolState startState, Blo } - - @SuppressWarnings("unused") public ProcessInvitationOrMembersUpdateStep(INeedMoreSeedsState startState, InvitationOrMembersUpdateMessage receivedMessage, GroupsV2Protocol protocol) throws Exception { super(ReceptionChannelInfo.createAnyObliviousChannelInfo(), receivedMessage, protocol); @@ -2161,8 +2200,6 @@ public ProcessInvitationOrMembersUpdateStep(INeedMoreSeedsState startState, Blob } - - @SuppressWarnings("unused") public ProcessInvitationOrMembersUpdateStep(InvitationReceivedState startState, InvitationOrMembersUpdateMessage receivedMessage, GroupsV2Protocol protocol) throws Exception { super(ReceptionChannelInfo.createAnyObliviousChannelInfo(), receivedMessage, protocol); @@ -2277,8 +2314,6 @@ public ProcessInvitationOrMembersUpdateStep(InvitationReceivedState startState, } - - @Override public ConcreteProtocolState executeStep() throws Exception { ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); @@ -2300,14 +2335,16 @@ public ConcreteProtocolState executeStep() throws Exception { UID[] otherOwnedDeviceUids = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); HashSet notNotifiedUids = new HashSet<>(Arrays.asList(otherOwnedDeviceUids)); - for (UID deviceUid : notifiedDeviceUids){ + for (UID deviceUid : notifiedDeviceUids) { notNotifiedUids.remove(deviceUid); } if (notNotifiedUids.size() > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createObliviousChannelInfo(getOwnedIdentity(), getOwnedIdentity(), notNotifiedUids.toArray(new UID[0]), true)); - ChannelMessageToSend messageToSend = new InvitationOrMembersUpdatePropagatedMessage(coreProtocolMessage, groupIdentifier, groupVersion, blobKeys, obliviousChannelContactIdentity).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createObliviousChannelInfo(getOwnedIdentity(), getOwnedIdentity(), notNotifiedUids.toArray(new UID[0]), true)); + ChannelMessageToSend messageToSend = new InvitationOrMembersUpdatePropagatedMessage(coreProtocolMessage, groupIdentifier, groupVersion, blobKeys, obliviousChannelContactIdentity).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } } @@ -2554,7 +2591,6 @@ public ConcreteProtocolState executeStep() throws Exception { } - /////////////// // from here we have everything: // - the blob @@ -2576,6 +2612,11 @@ public ConcreteProtocolState executeStep() throws Exception { return null; } + // if the update was initiated by another of our own devices, auto-trust the details + if (Objects.equals(signerIdentity, getOwnedIdentity())) { + protocolManagerSession.identityDelegate.trustGroupV2PublishedDetails(protocolManagerSession.session, getOwnedIdentity(), startState.groupIdentifier); + } + protocolManagerSession.identityDelegate.unfreezeGroupV2(protocolManagerSession.session, getOwnedIdentity(), startState.groupIdentifier); // check if a photo download is needed @@ -2608,6 +2649,50 @@ public ConcreteProtocolState executeStep() throws Exception { } } + return new FinalState(); + } else if (serverBlob.administratorsChain.isChainCreatedBy(getOwnedIdentity()) && startState.invitationCollectedData.inviterIdentityAndBlobMainSeedCandidates.containsKey(getOwnedIdentity())) { + // check who signed the first block of the administrator chain --> this is the group creator + // also check that one of the seed was sent by me --> this means I am the group creator (on another device) + // => in this case, create the group directly and start the photo download + + // create the group in DB (we use the createJoinedGroupV2 method which is better suited here, even if another of my devices created the group) + boolean success = protocolManagerSession.identityDelegate.createJoinedGroupV2(protocolManagerSession.session, getOwnedIdentity(), startState.groupIdentifier, blobKeys, serverBlob, true); + + // if success == false, this is not a retry-able failure, so we do nothing + if (success) { + // getGroupV2PhotoUrl will always return null here, but we check anyways + if (serverBlob.serverPhotoInfo != null && protocolManagerSession.identityDelegate.getGroupV2PhotoUrl(protocolManagerSession.session, getOwnedIdentity(), startState.groupIdentifier) == null) { + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage( + SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), + DOWNLOAD_GROUPS_V2_PHOTO_PROTOCOL_ID, + new UID(getPrng()), + false + ); + ChannelMessageToSend messageToSend = new DownloadGroupV2PhotoProtocol.InitialMessage(coreProtocolMessage, startState.groupIdentifier, serverBlob.serverPhotoInfo).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } + + // send a ping to all members to notify them you indeed joined the group + for (GroupV2.IdentityAndPermissionsAndDetails groupMember : serverBlob.groupMemberIdentityAndPermissionsAndDetailsList) { + if (groupMember.identity.equals(getOwnedIdentity())) { + continue; + } + + byte[] pingSignature = protocolManagerSession.identityDelegate.signGroupInvitationNonce( + protocolManagerSession.session, + Constants.SignatureContext.GROUP_JOIN_NONCE, + startState.groupIdentifier, + ownIdentityAndPermissions.groupInvitationNonce, + groupMember.identity, + getOwnedIdentity(), + getPrng()); + + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAsymmetricBroadcastChannelInfo(groupMember.identity, getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PingMessage(coreProtocolMessage, startState.groupIdentifier, ownIdentityAndPermissions.groupInvitationNonce, pingSignature, false).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } + } + return new FinalState(); } else { { @@ -2726,9 +2811,11 @@ public ConcreteProtocolState executeStep() throws Exception { // propagate the ping to other own devices, if any int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagatedPingMessage(coreProtocolMessage, receivedMessage.groupIdentifier, receivedMessage.groupMemberInvitationNonce, receivedMessage.signature, receivedMessage.isResponse).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagatedPingMessage(coreProtocolMessage, receivedMessage.groupIdentifier, receivedMessage.groupMemberInvitationNonce, receivedMessage.signature, receivedMessage.isResponse).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } @@ -2932,9 +3019,11 @@ public ConcreteProtocolState executeStep() throws Exception { // propagate the dialog response to other devices int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagateInvitationDialogResponseMessage(coreProtocolMessage, this.invitationAccepted, ownGroupInvitationNonce).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateInvitationDialogResponseMessage(coreProtocolMessage, this.invitationAccepted, ownGroupInvitationNonce).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } @@ -2943,7 +3032,7 @@ public ConcreteProtocolState executeStep() throws Exception { ((InvitationReceivedState) startState).serverBlob.administratorsChain.integrityWasChecked = true; // create the group in db - boolean success = protocolManagerSession.identityDelegate.createJoinedGroupV2(protocolManagerSession.session, getOwnedIdentity(), groupIdentifier, ((InvitationReceivedState) startState).blobKeys, ((InvitationReceivedState) startState).serverBlob); + boolean success = protocolManagerSession.identityDelegate.createJoinedGroupV2(protocolManagerSession.session, getOwnedIdentity(), groupIdentifier, ((InvitationReceivedState) startState).blobKeys, ((InvitationReceivedState) startState).serverBlob, false); // if success == false, this is not a retry-able failure, so we do nothing if (success) { @@ -3139,9 +3228,11 @@ public ConcreteProtocolState executeStep() throws Exception { if (propagationNeeded) { int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagateInvitationRejectedMessage(coreProtocolMessage, groupIdentifier).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateInvitationRejectedMessage(coreProtocolMessage, groupIdentifier).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } @@ -3438,7 +3529,8 @@ public ConcreteProtocolState executeStep() throws Exception { for (byte[] bytesIdentity : startState.changeSet.removedMembers) { try { removedMembersSet.add(Identity.of(bytesIdentity)); - } catch (DecodingException ignored) {} + } catch (DecodingException ignored) { + } } List toRemove = new ArrayList<>(); @@ -3480,7 +3572,8 @@ public ConcreteProtocolState executeStep() throws Exception { for (GroupV2.Permission permission : newPermissions) { member.permissionStrings.add(permission.getString()); } - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } } @@ -3490,12 +3583,12 @@ public ConcreteProtocolState executeStep() throws Exception { String serializedDetails; if (Objects.equals(member.identity, getOwnedIdentity())) { serializedDetails = protocolManagerSession.identityDelegate.getSerializedPublishedDetailsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); - } else{ + } else { serializedDetails = protocolManagerSession.identityDelegate.getSerializedPublishedDetailsOfContactIdentity(protocolManagerSession.session, getOwnedIdentity(), member.identity); } if (serializedDetails != null && !Objects.equals(member.serializedIdentityDetails, serializedDetails)) { - GroupV2.IdentityAndPermissionsAndDetails updatedMember = new GroupV2.IdentityAndPermissionsAndDetails(member.identity, member.permissionStrings, serializedDetails, member.groupInvitationNonce); - updatedMembers.add(updatedMember); + GroupV2.IdentityAndPermissionsAndDetails updatedMember = new GroupV2.IdentityAndPermissionsAndDetails(member.identity, member.permissionStrings, serializedDetails, member.groupInvitationNonce); + updatedMembers.add(updatedMember); } } for (GroupV2.IdentityAndPermissionsAndDetails updatedMember : updatedMembers) { @@ -3531,14 +3624,19 @@ public ConcreteProtocolState executeStep() throws Exception { changed = true; members.add(newMember); membersToInvite.add(memberIdentity); - } catch (Exception ignored) {} - } + } catch (Exception ignored) { + } + } // group details if (startState.changeSet.updatedSerializedGroupDetails != null && !Objects.equals(startState.changeSet.updatedSerializedGroupDetails, initialServerBlob.serializedGroupDetails)) { changed = true; } + // group type + if (startState.changeSet.updatedJsonGroupType != null && !Objects.equals(startState.changeSet.updatedJsonGroupType, initialServerBlob.serializedGroupType)) { + changed = true; + } // group photoUrl if (startState.changeSet.updatedPhotoUrl != null && (initialServerBlob.serverPhotoInfo != null || startState.changeSet.updatedPhotoUrl.length() > 0)) { changed = true; @@ -3605,6 +3703,13 @@ public ConcreteProtocolState executeStep() throws Exception { updatedSerializedGroupDetails = initialServerBlob.serializedGroupDetails; } + String updatedJsonGroupType; + if (startState.changeSet.updatedJsonGroupType != null) { + updatedJsonGroupType = startState.changeSet.updatedJsonGroupType; + } else { + updatedJsonGroupType = initialServerBlob.serializedGroupType; + } + GroupV2.ServerPhotoInfo updatedServerPhotoInfo; if (startState.changeSet.updatedPhotoUrl != null && startState.changeSet.updatedPhotoUrl.length() == 0) { // photo was removed @@ -3643,7 +3748,8 @@ public ConcreteProtocolState executeStep() throws Exception { members, initialServerBlob.version + 1, updatedSerializedGroupDetails, - updatedServerPhotoInfo + updatedServerPhotoInfo, + updatedJsonGroupType ); } @@ -3775,7 +3881,7 @@ public ConcreteProtocolState executeStep() throws Exception { return new WaitingForLockState(startState.groupIdentifier, startState.changeSet, lockNonce, startState.failedUploadCounter + 1); } - + if (startState.absolutePhotoUrlToUpload == null) { // if there is no photo to upload, post a message to initiate the finalization of the group update CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(getOwnedIdentity())); @@ -3857,11 +3963,12 @@ public ConcreteProtocolState executeStep() throws Exception { // - the version seed for everyone // - for admins the groupAdmin private key + // here we loop on ALL group members, including ourself for (GroupV2.IdentityAndPermissionsAndDetails groupMember : startState.updatedBlob.groupMemberIdentityAndPermissionsAndDetailsList) { - UID[] contactDeviceUidsWithChannel = protocolManagerSession.channelDelegate.getConfirmedObliviousChannelDeviceUids(protocolManagerSession.session, getOwnedIdentity(), groupMember.identity); + UID[] contactOrOwnedDeviceUidsWithChannel = protocolManagerSession.channelDelegate.getConfirmedObliviousChannelDeviceUids(protocolManagerSession.session, getOwnedIdentity(), groupMember.identity); boolean isAdmin = groupMember.permissionStrings.contains(GroupV2.Permission.GROUP_ADMIN.getString()); - if (contactDeviceUidsWithChannel.length > 0) { + if (contactOrOwnedDeviceUidsWithChannel.length > 0) { // send through oblivious channel GroupV2.BlobKeys keysToSend; if (isAdmin) { @@ -3875,9 +3982,9 @@ public ConcreteProtocolState executeStep() throws Exception { } CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllConfirmedObliviousChannelsInfo(groupMember.identity, getOwnedIdentity())); - ChannelMessageToSend messageToSend = new InvitationOrMembersUpdateMessage(coreProtocolMessage, startState.groupIdentifier, startState.updatedBlob.version, keysToSend, contactDeviceUidsWithChannel).generateChannelProtocolMessageToSend(); + ChannelMessageToSend messageToSend = new InvitationOrMembersUpdateMessage(coreProtocolMessage, startState.groupIdentifier, startState.updatedBlob.version, keysToSend, contactOrOwnedDeviceUidsWithChannel).generateChannelProtocolMessageToSend(); protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); - } else { + } else if (!Objects.equals(groupMember.identity, getOwnedIdentity())) { // never send a broadcast message to our own devices // send through broadcast channel GroupV2.BlobKeys keysToSend = new GroupV2.BlobKeys( null, @@ -4023,9 +4130,11 @@ public ConcreteProtocolState executeStep() throws Exception { // propagate the kick message int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagatedKickMessage(coreProtocolMessage, receivedMessage.groupIdentifier, receivedMessage.encryptedAdministratorsChain, receivedMessage.signature).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagatedKickMessage(coreProtocolMessage, receivedMessage.groupIdentifier, receivedMessage.encryptedAdministratorsChain, receivedMessage.signature).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } @@ -4206,10 +4315,13 @@ public ConcreteProtocolState executeStep() throws Exception { } { - // check I am not the only admin - boolean admin = protocolManagerSession.identityDelegate.getGroupV2AdminStatus(protocolManagerSession.session, getOwnedIdentity(), this.groupIdentifier); - if (admin && !protocolManagerSession.identityDelegate.getGroupV2HasOtherAdminMember(protocolManagerSession.session, getOwnedIdentity(), this.groupIdentifier)) { - return startState; + // if group is not frozen, check I am not the only admin + boolean frozen = protocolManagerSession.identityDelegate.isGroupV2Frozen(protocolManagerSession.session, getOwnedIdentity(), this.groupIdentifier); + if (!frozen) { + boolean admin = protocolManagerSession.identityDelegate.getGroupV2AdminStatus(protocolManagerSession.session, getOwnedIdentity(), this.groupIdentifier); + if (admin && !protocolManagerSession.identityDelegate.getGroupV2HasOtherAdminMember(protocolManagerSession.session, getOwnedIdentity(), this.groupIdentifier)) { + return startState; + } } } @@ -4219,9 +4331,11 @@ public ConcreteProtocolState executeStep() throws Exception { // propagate the group leave message to other devices int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagatedGroupLeaveMessage(coreProtocolMessage, this.groupIdentifier, ownGroupInvitationNonce).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagatedGroupLeaveMessage(coreProtocolMessage, this.groupIdentifier, ownGroupInvitationNonce).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } // put a group left log on server @@ -4412,9 +4526,11 @@ public ConcreteProtocolState executeStep() throws Exception { // propagate the disband request int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagatedGroupDisbandMessage(coreProtocolMessage, startState.groupIdentifier).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagatedGroupDisbandMessage(coreProtocolMessage, startState.groupIdentifier).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } @@ -4471,7 +4587,7 @@ public ConcreteProtocolState executeStep() throws Exception { GroupV2.IdentifierVersionAndKeys[] identifierVersionAndKeys = protocolManagerSession.identityDelegate.getServerGroupsV2IdentifierVersionAndKeysForContact(protocolManagerSession.session, getOwnedIdentity(), receivedMessage.contactIdentity); if (identifierVersionAndKeys.length > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createObliviousChannelInfo(receivedMessage.contactIdentity, getOwnedIdentity(), new UID[] {receivedMessage.contactDeviceUid}, false)); + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createObliviousChannelInfo(receivedMessage.contactIdentity, getOwnedIdentity(), new UID[]{receivedMessage.contactDeviceUid}, false)); ChannelMessageToSend messageToSend = new BlobKeysBatchAfterChannelCreationMessage(coreProtocolMessage, identifierVersionAndKeys).generateChannelProtocolMessageToSend(); protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); } @@ -4481,7 +4597,6 @@ public ConcreteProtocolState executeStep() throws Exception { } - public static class ProcessBatchKeysMessageStep extends ProtocolStep { @SuppressWarnings({"unused", "FieldCanBeLocal"}) private final InitialProtocolState startState; diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/IdentityDetailsPublicationProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/IdentityDetailsPublicationProtocol.java index 47c42fbe..90435266 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/IdentityDetailsPublicationProtocol.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/IdentityDetailsPublicationProtocol.java @@ -29,6 +29,7 @@ import io.olvid.engine.crypto.PRNGService; import io.olvid.engine.crypto.Suite; import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.datatypes.NoAcceptableChannelException; import io.olvid.engine.datatypes.UID; import io.olvid.engine.datatypes.containers.ChannelMessageToSend; import io.olvid.engine.datatypes.containers.ReceptionChannelInfo; @@ -162,6 +163,7 @@ public Encoded encode() { public static final int INITIAL_MESSAGE_ID = 0; public static final int SERVER_PUT_PHOTO_MESSAGE_ID = 1; public static final int SEND_DETAILS_MESSAGE_ID = 2; + public static final int PROPAGATE_OWN_DETAILS_MESSAGE_ID = 3; @Override protected Class getMessageClass(int protocolMessageId) { @@ -172,6 +174,8 @@ protected Class getMessageClass(int protocolMessageId) { return ServerPutPhotoMessage.class; case SEND_DETAILS_MESSAGE_ID: return SendDetailsMessage.class; + case PROPAGATE_OWN_DETAILS_MESSAGE_ID: + return PropagateOwnDetailsMessage.class; default: return null; } @@ -259,6 +263,36 @@ public Encoded[] getInputs() { } + public static class PropagateOwnDetailsMessage extends ConcreteProtocolMessage { + private final String jsonIdentityDetailsWithVersionAndPhoto; + + public PropagateOwnDetailsMessage(CoreProtocolMessage coreProtocolMessage, String jsonIdentityDetailsWithVersionAndPhoto) { + super(coreProtocolMessage); + this.jsonIdentityDetailsWithVersionAndPhoto = jsonIdentityDetailsWithVersionAndPhoto; + } + + public PropagateOwnDetailsMessage(ReceivedMessage receivedMessage) throws Exception { + super(new CoreProtocolMessage(receivedMessage)); + if (receivedMessage.getInputs().length != 1) { + throw new Exception(); + } + this.jsonIdentityDetailsWithVersionAndPhoto = receivedMessage.getInputs()[0].decodeString(); + } + + @Override + public int getProtocolMessageId() { + return PROPAGATE_OWN_DETAILS_MESSAGE_ID; + } + + @Override + public Encoded[] getInputs() { + return new Encoded[]{ + Encoded.of(jsonIdentityDetailsWithVersionAndPhoto), + }; + } + } + + // endregion @@ -272,7 +306,7 @@ public Encoded[] getInputs() { protected Class[] getPossibleStepClasses(int stateId) { switch (stateId) { case INITIAL_STATE_ID: - return new Class[]{StartPhotoUploadStep.class, ReceiveDetailsStep.class}; + return new Class[]{StartPhotoUploadStep.class, ReceiveDetailsStep.class, ReceiveOwnDetailsStep.class}; case UPLOADING_PHOTO_STATE_ID: return new Class[]{SendDetailsStep.class}; case DETAILS_SENT_STATE_ID: @@ -331,10 +365,10 @@ public ConcreteProtocolState executeStep() throws Exception { return new UploadingPhotoState(jsonPublishedDetails); } else { // we can directly send the details + String jsonPublishedDetails = protocol.getJsonObjectMapper().writeValueAsString(publishedDetails); Identity[] contactIdentities = protocolManagerSession.identityDelegate.getContactsOfOwnedIdentity(protocolManagerSession.session, ownedIdentity); if (contactIdentities.length > 0) { - String jsonPublishedDetails = protocol.getJsonObjectMapper().writeValueAsString(publishedDetails); SendChannelInfo[] sendChannelInfos = SendChannelInfo.createAllConfirmedObliviousChannelsInfosForMultipleIdentities(contactIdentities, ownedIdentity); for (SendChannelInfo sendChannelInfo : sendChannelInfos) { @@ -348,6 +382,18 @@ public ConcreteProtocolState executeStep() throws Exception { } } + { + // send the details to other owned devices + int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; + if (numberOfOtherDevices > 0) { + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateOwnDetailsMessage(coreProtocolMessage, jsonPublishedDetails).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } + } + } + return new DetailsSentState(); } } @@ -367,19 +413,34 @@ public SendDetailsStep(UploadingPhotoState startState, ServerPutPhotoMessage rec public ConcreteProtocolState executeStep() throws Exception { ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); - Identity[] contactIdentities = protocolManagerSession.identityDelegate.getContactsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); - if (contactIdentities.length > 0) { - SendChannelInfo[] sendChannelInfos = SendChannelInfo.createAllConfirmedObliviousChannelsInfosForMultipleIdentities(contactIdentities, getOwnedIdentity()); - for (SendChannelInfo sendChannelInfo : sendChannelInfos) { + { + Identity[] contactIdentities = protocolManagerSession.identityDelegate.getContactsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); + if (contactIdentities.length > 0) { + SendChannelInfo[] sendChannelInfos = SendChannelInfo.createAllConfirmedObliviousChannelsInfosForMultipleIdentities(contactIdentities, getOwnedIdentity()); + for (SendChannelInfo sendChannelInfo : sendChannelInfos) { + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(sendChannelInfo); + ChannelMessageToSend messageToSend = new SendDetailsMessage(coreProtocolMessage, startState.jsonIdentityDetailsWithVersionAndPhoto).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (Exception e) { + Logger.d("One contact with no channel during IdentityDetailsPublicationProtocol.SendDetailsStep"); + } + } + } + } + + { + // send the details to other owned devices + int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; + if (numberOfOtherDevices > 0) { try { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(sendChannelInfo); - ChannelMessageToSend messageToSend = new SendDetailsMessage(coreProtocolMessage, startState.jsonIdentityDetailsWithVersionAndPhoto).generateChannelProtocolMessageToSend(); + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateOwnDetailsMessage(coreProtocolMessage, startState.jsonIdentityDetailsWithVersionAndPhoto).generateChannelProtocolMessageToSend(); protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); - } catch (Exception e) { - Logger.d("One contact with no channel during IdentityDetailsPublicationProtocol.SendDetailsStep"); - } + } catch (NoAcceptableChannelException ignored) { } } } + return new DetailsSentState(); } } @@ -408,8 +469,8 @@ public ConcreteProtocolState executeStep() throws Exception { if (! (Arrays.equals(jsonIdentityDetailsWithVersionAndPhoto.getPhotoServerLabel(), publishedDetails.getPhotoServerLabel()) && ((jsonIdentityDetailsWithVersionAndPhoto.getPhotoServerKey() == null && publishedDetails.getPhotoServerKey() == null) || - (jsonIdentityDetailsWithVersionAndPhoto.getPhotoServerKey() != null && publishedDetails.getPhotoServerKey() != null && new Encoded(jsonIdentityDetailsWithVersionAndPhoto.getPhotoServerKey()).decodeSymmetricKey().equals(new Encoded(publishedDetails.getPhotoServerKey()).decodeSymmetricKey()))) && - publishedDetails.getPhotoUrl() != null)) { + (jsonIdentityDetailsWithVersionAndPhoto.getPhotoServerKey() != null && publishedDetails.getPhotoServerKey() != null && new Encoded(jsonIdentityDetailsWithVersionAndPhoto.getPhotoServerKey()).decodeSymmetricKey().equals(new Encoded(publishedDetails.getPhotoServerKey()).decodeSymmetricKey()))) && + publishedDetails.getPhotoUrl() != null)) { // we need to download the photo, so we start a child protocol CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage( @@ -430,5 +491,44 @@ public ConcreteProtocolState executeStep() throws Exception { } } + public static class ReceiveOwnDetailsStep extends ProtocolStep { + private final InitialProtocolState startState; + private final PropagateOwnDetailsMessage receivedMessage; + + public ReceiveOwnDetailsStep(InitialProtocolState startState, PropagateOwnDetailsMessage receivedMessage, IdentityDetailsPublicationProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createAnyObliviousChannelWithOwnedDeviceInfo(), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + Identity ownedIdentity = getOwnedIdentity(); + JsonIdentityDetailsWithVersionAndPhoto ownDetailsWithVersionAndPhoto = protocol.getJsonObjectMapper().readValue(receivedMessage.jsonIdentityDetailsWithVersionAndPhoto, JsonIdentityDetailsWithVersionAndPhoto.class); + + // update the published details + boolean photoDownloadNeeded = protocolManagerSession.identityDelegate.setOwnedIdentityDetailsFromOtherDevice(protocolManagerSession.session, ownedIdentity, ownDetailsWithVersionAndPhoto); + + if (photoDownloadNeeded) { + // even though another device set the photo, we create a ServerUserData to ensure this photo is retained on server + protocolManagerSession.identityDelegate.createOwnedIdentityServerUserData(protocolManagerSession.session, getOwnedIdentity(), new UID(ownDetailsWithVersionAndPhoto.getPhotoServerLabel())); + + // we need to download a photo + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage( + SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), + DOWNLOAD_IDENTITY_PHOTO_CHILD_PROTOCOL_ID, + new UID(getPrng()), + false + ); + ChannelMessageToSend messageToSend = new DownloadIdentityPhotoChildProtocol.InitialMessage(coreProtocolMessage, getOwnedIdentity(), receivedMessage.jsonIdentityDetailsWithVersionAndPhoto).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } + + return new DetailsReceivedState(); + } + } + // endregion } \ No newline at end of file diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/KeycloakBindingAndUnbindingProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/KeycloakBindingAndUnbindingProtocol.java index c51985c5..a14f8aff 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/KeycloakBindingAndUnbindingProtocol.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/KeycloakBindingAndUnbindingProtocol.java @@ -21,13 +21,18 @@ import com.fasterxml.jackson.databind.ObjectMapper; +import org.jose4j.jwk.JsonWebKey; +import org.jose4j.jwk.JsonWebKeySet; + import io.olvid.engine.Logger; import io.olvid.engine.crypto.PRNGService; import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.datatypes.NoAcceptableChannelException; import io.olvid.engine.datatypes.UID; import io.olvid.engine.datatypes.containers.ChannelMessageToSend; import io.olvid.engine.datatypes.containers.ReceptionChannelInfo; import io.olvid.engine.datatypes.containers.SendChannelInfo; +import io.olvid.engine.datatypes.notifications.ProtocolNotifications; import io.olvid.engine.encoder.Encoded; import io.olvid.engine.engine.types.identities.ObvKeycloakState; import io.olvid.engine.protocol.databases.ReceivedMessage; @@ -41,6 +46,8 @@ import static io.olvid.engine.protocol.protocols.KeycloakContactAdditionProtocol.FINISHED_STATE_ID; +import java.util.HashMap; + public class KeycloakBindingAndUnbindingProtocol extends ConcreteProtocol { public KeycloakBindingAndUnbindingProtocol(ProtocolManagerSession protocolManagerSession, UID protocolInstanceUid, int currentStateId, Encoded encodedCurrentState, Identity ownedIdentity, PRNGService prng, ObjectMapper jsonObjectMapper) throws Exception { super(protocolManagerSession, protocolInstanceUid, currentStateId, encodedCurrentState, ownedIdentity, prng, jsonObjectMapper); @@ -99,13 +106,11 @@ public Encoded encode() { // endregion - - - // region messages static final int OWNED_IDENTITY_KEYCLOAK_BINDING_MESSAGE_ID = 0; static final int OWNED_IDENTITY_KEYCLOAK_UNBINDING_MESSAGE_ID = 1; - + static final int PROPAGATE_KEYCLOAK_BINDING_MESSAGE_ID = 2; + static final int PROPAGATE_KEYCLOAK_UNBINDING_MESSAGE_ID = 3; @Override protected Class getMessageClass(int protocolMessageId) { @@ -114,6 +119,10 @@ protected Class getMessageClass(int protocolMessageId) { return OwnedIdentityKeycloakBindingMessage.class; case OWNED_IDENTITY_KEYCLOAK_UNBINDING_MESSAGE_ID: return OwnedIdentityKeycloakUnbindingMessage.class; + case PROPAGATE_KEYCLOAK_BINDING_MESSAGE_ID: + return PropagateKeycloakBindingMessage.class; + case PROPAGATE_KEYCLOAK_UNBINDING_MESSAGE_ID: + return PropagateKeycloakUnbindingMessage.class; default: return null; } @@ -179,11 +188,82 @@ public Encoded[] getInputs() { } } - // endregion + public static class PropagateKeycloakBindingMessage extends ConcreteProtocolMessage { + public final String keycloakUserId; + public final String keycloakServer; + public final String clientId; + public final String clientSecret; // may be null --> encoded as an empty String in this case + public final String jwks; + public final String signatureKey; + + public PropagateKeycloakBindingMessage(CoreProtocolMessage coreProtocolMessage, String keycloakUserId, ObvKeycloakState keycloakState) { + super(coreProtocolMessage); + this.keycloakUserId = keycloakUserId; + this.keycloakServer = keycloakState.keycloakServer; + this.clientId = keycloakState.clientId; + this.clientSecret = keycloakState.clientSecret; + this.jwks = keycloakState.jwks.toJson(); + this.signatureKey = keycloakState.signatureKey.toJson(); + } + + @SuppressWarnings({"unused", "RedundantSuppression"}) + public PropagateKeycloakBindingMessage(ReceivedMessage receivedMessage) throws Exception { + super(new CoreProtocolMessage(receivedMessage)); + if (receivedMessage.getInputs().length != 6) { + throw new Exception(); + } + this.keycloakUserId = receivedMessage.getInputs()[0].decodeString(); + this.keycloakServer = receivedMessage.getInputs()[1].decodeString(); + this.clientId = receivedMessage.getInputs()[2].decodeString(); + String clientSecret = receivedMessage.getInputs()[3].decodeString(); + this.clientSecret = clientSecret.length() == 0 ? null : clientSecret; + this.jwks = receivedMessage.getInputs()[4].decodeString(); + this.signatureKey = receivedMessage.getInputs()[5].decodeString(); + } + + @Override + public int getProtocolMessageId() { + return PROPAGATE_KEYCLOAK_BINDING_MESSAGE_ID; + } + + @Override + public Encoded[] getInputs() { + return new Encoded[]{ + Encoded.of(keycloakUserId), + Encoded.of(keycloakServer), + Encoded.of(clientId), + Encoded.of(clientSecret == null ? "" : clientSecret), + Encoded.of(jwks), + Encoded.of(signatureKey), + }; + } + } + + public static class PropagateKeycloakUnbindingMessage extends ConcreteProtocolMessage { + public PropagateKeycloakUnbindingMessage(CoreProtocolMessage coreProtocolMessage) { + super(coreProtocolMessage); + } + @SuppressWarnings({"unused", "RedundantSuppression"}) + public PropagateKeycloakUnbindingMessage(ReceivedMessage receivedMessage) throws Exception { + super(new CoreProtocolMessage(receivedMessage)); + if (receivedMessage.getInputs().length != 0) { + throw new Exception(); + } + } + @Override + public int getProtocolMessageId() { + return PROPAGATE_KEYCLOAK_UNBINDING_MESSAGE_ID; + } + @Override + public Encoded[] getInputs() { + return new Encoded[0]; + } + } + // endregion // region steps @@ -200,12 +280,35 @@ protected Class[] getPossibleStepClasses(int stateId) { public static class OwnedIdentityKeycloakBindingStep extends ProtocolStep { InitialProtocolState startState; - OwnedIdentityKeycloakBindingMessage receivedMessage; + String keycloakUserId; + ObvKeycloakState keycloakState; + boolean propagationNeeded; + public OwnedIdentityKeycloakBindingStep(InitialProtocolState startState, OwnedIdentityKeycloakBindingMessage receivedMessage, KeycloakBindingAndUnbindingProtocol protocol) throws Exception { super(ReceptionChannelInfo.createLocalChannelInfo(), receivedMessage, protocol); this.startState = startState; - this.receivedMessage = receivedMessage; + this.keycloakUserId = receivedMessage.keycloakUserId; + this.keycloakState = receivedMessage.keycloakState; + this.propagationNeeded = true; + } + + public OwnedIdentityKeycloakBindingStep(InitialProtocolState startState, PropagateKeycloakBindingMessage receivedMessage, KeycloakBindingAndUnbindingProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createAnyObliviousChannelWithOwnedDeviceInfo(), receivedMessage, protocol); + this.startState = startState; + this.keycloakUserId = receivedMessage.keycloakUserId; + this.keycloakState = new ObvKeycloakState( + receivedMessage.keycloakServer, + receivedMessage.clientId, + receivedMessage.clientSecret, + new JsonWebKeySet(receivedMessage.jwks), + JsonWebKey.Factory.newJwk(receivedMessage.signatureKey), + null, + null, + 0, + 0 + ); + this.propagationNeeded = false; } @Override @@ -214,10 +317,10 @@ public ConcreteProtocolState executeStep() throws Exception { ///////// // re-check all inputs - if (receivedMessage.keycloakUserId == null - || receivedMessage.keycloakState == null - || receivedMessage.keycloakState.keycloakServer == null - || receivedMessage.keycloakState.jwks == null) { + if (keycloakUserId == null + || keycloakState == null + || keycloakState.keycloakServer == null + || keycloakState.jwks == null) { Logger.w("Bad inputs for OwnedIdentityKeycloakBindingStep, aborting."); return new FinishedProtocolState(); } @@ -229,14 +332,35 @@ public ConcreteProtocolState executeStep() throws Exception { protocolManagerSession.identityDelegate.bindOwnedIdentityToKeycloak( protocolManagerSession.session, getOwnedIdentity(), - receivedMessage.keycloakUserId, - receivedMessage.keycloakState); + keycloakUserId, + keycloakState); ///////// // re-check all contacts protocolManagerSession.identityDelegate.reCheckAllCertifiedByOwnKeycloakContacts(protocolManagerSession.session, getOwnedIdentity()); + ////////// + // propagate the binding to other owned devices (if any) + if (propagationNeeded) { + int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; + if (numberOfOtherDevices > 0) { + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateKeycloakBindingMessage(coreProtocolMessage, keycloakUserId, keycloakState).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { + } + } + } else { + // notify the app that a keycloak registration & synchronization is required + protocolManagerSession.session.addSessionCommitListener(() -> { + HashMap userInfo = new HashMap<>(); + userInfo.put(ProtocolNotifications.NOTIFICATION_KEYCLOAK_SYNCHRONIZATION_REQUIRED_OWNED_IDENTITY_KEY, getOwnedIdentity()); + protocolManagerSession.notificationPostingDelegate.postNotification(ProtocolNotifications.NOTIFICATION_KEYCLOAK_SYNCHRONIZATION_REQUIRED, userInfo); + }); + } + return new FinishedProtocolState(); } } @@ -244,12 +368,18 @@ public ConcreteProtocolState executeStep() throws Exception { public static class OwnedIdentityKeycloakUnbindingStep extends ProtocolStep { InitialProtocolState startState; - OwnedIdentityKeycloakUnbindingMessage receivedMessage; + boolean propagationNeeded; public OwnedIdentityKeycloakUnbindingStep(InitialProtocolState startState, OwnedIdentityKeycloakUnbindingMessage receivedMessage, KeycloakBindingAndUnbindingProtocol protocol) throws Exception { super(ReceptionChannelInfo.createLocalChannelInfo(), receivedMessage, protocol); this.startState = startState; - this.receivedMessage = receivedMessage; + this.propagationNeeded = true; + } + + public OwnedIdentityKeycloakUnbindingStep(InitialProtocolState startState, PropagateKeycloakUnbindingMessage receivedMessage, KeycloakBindingAndUnbindingProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createAnyObliviousChannelWithOwnedDeviceInfo(), receivedMessage, protocol); + this.startState = startState; + this.propagationNeeded = false; } @Override @@ -290,6 +420,20 @@ public ConcreteProtocolState executeStep() throws Exception { protocolManagerSession.identityDelegate.unmarkAllCertifiedByOwnKeycloakContacts(protocolManagerSession.session, getOwnedIdentity()); } + ////////// + // propagate the unbinding to other owned devices (if any) + if (propagationNeeded) { + int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; + if (numberOfOtherDevices > 0) { + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateKeycloakUnbindingMessage(coreProtocolMessage).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { + } + } + } + return new FinishedProtocolState(); } } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/KeycloakContactAdditionProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/KeycloakContactAdditionProtocol.java index 7eeea577..76cfdf42 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/KeycloakContactAdditionProtocol.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/KeycloakContactAdditionProtocol.java @@ -26,6 +26,7 @@ import io.olvid.engine.crypto.PRNGService; import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.datatypes.NoAcceptableChannelException; import io.olvid.engine.datatypes.UID; import io.olvid.engine.datatypes.containers.ChannelMessageToSend; import io.olvid.engine.datatypes.containers.ReceptionChannelInfo; @@ -602,7 +603,7 @@ public ConcreteProtocolState executeStep() throws Exception { protocolManagerSession.identityDelegate.addContactIdentity(protocolManagerSession.session, startState.contactIdentity, startState.contactSerializedDetails, getOwnedIdentity(), TrustOrigin.createKeycloakTrustOrigin(trustTimestamp, startState.keycloakServerUrl), true); for (UID contactDeviceUid : contactDeviceUids) { - protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), startState.contactIdentity, contactDeviceUid); + protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), startState.contactIdentity, contactDeviceUid, false); } } else { contactCreated = false; @@ -617,9 +618,11 @@ public ConcreteProtocolState executeStep() throws Exception { { int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagateContactAdditionToOtherDevicesMessage(coreProtocolMessage, startState.contactIdentity, startState.keycloakServerUrl, startState.contactSerializedDetails, contactDeviceUids, trustTimestamp).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateContactAdditionToOtherDevicesMessage(coreProtocolMessage, startState.contactIdentity, startState.keycloakServerUrl, startState.contactSerializedDetails, contactDeviceUids, trustTimestamp).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } @@ -665,7 +668,7 @@ public ConcreteProtocolState executeStep() throws Exception { protocolManagerSession.identityDelegate.addContactIdentity(protocolManagerSession.session, receivedMessage.contactIdentity, receivedMessage.contactSerializedDetails, getOwnedIdentity(), TrustOrigin.createKeycloakTrustOrigin(receivedMessage.trustTimestamp, receivedMessage.keycloakServerUrl), true); for (UID contactDeviceUid : receivedMessage.contactDeviceUids) { - protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), receivedMessage.contactIdentity, contactDeviceUid); + protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), receivedMessage.contactIdentity, contactDeviceUid, false); } } else { protocolManagerSession.identityDelegate.addTrustOriginToContact(protocolManagerSession.session, receivedMessage.contactIdentity, getOwnedIdentity(), TrustOrigin.createKeycloakTrustOrigin(receivedMessage.trustTimestamp, receivedMessage.keycloakServerUrl), true); @@ -766,7 +769,7 @@ public ConcreteProtocolState executeStep() throws Exception { protocolManagerSession.identityDelegate.addContactIdentity(protocolManagerSession.session, startState.contactIdentity, startState.contactSerializedDetails, getOwnedIdentity(), TrustOrigin.createKeycloakTrustOrigin(System.currentTimeMillis(), startState.keycloakServerUrl), true); for (UID contactDeviceUid : startState.contactDeviceUids) { - protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), startState.contactIdentity, contactDeviceUid); + protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), startState.contactIdentity, contactDeviceUid, false); } } else { protocolManagerSession.identityDelegate.addTrustOriginToContact(protocolManagerSession.session, startState.contactIdentity, getOwnedIdentity(), TrustOrigin.createKeycloakTrustOrigin(System.currentTimeMillis(), startState.keycloakServerUrl), true); diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OneToOneContactInvitationProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OneToOneContactInvitationProtocol.java index 602ffa83..58127ee3 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OneToOneContactInvitationProtocol.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OneToOneContactInvitationProtocol.java @@ -27,6 +27,7 @@ import io.olvid.engine.Logger; import io.olvid.engine.crypto.PRNGService; import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.datatypes.NoAcceptableChannelException; import io.olvid.engine.datatypes.UID; import io.olvid.engine.datatypes.containers.ChannelMessageToSend; import io.olvid.engine.datatypes.containers.DialogType; @@ -677,9 +678,11 @@ public ConcreteProtocolState executeStep() throws Exception { // propagate invitation to you other owned devices (if any) int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagateOneToOneInvitationMessage(coreProtocolMessage, receivedMessage.contactIdentity).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateOneToOneInvitationMessage(coreProtocolMessage, receivedMessage.contactIdentity).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } @@ -813,9 +816,11 @@ public ConcreteProtocolState executeStep() throws Exception { // Propagate the answer to Bob's other devices int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagateOneToOneResponseMessage(coreProtocolMessage, receivedMessage.invitationAccepted).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateOneToOneResponseMessage(coreProtocolMessage, receivedMessage.invitationAccepted).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } @@ -907,9 +912,11 @@ public ConcreteProtocolState executeStep() throws Exception { // Propagate the abort to Alice's other devices int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagateAbortMessage(coreProtocolMessage).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateAbortMessage(coreProtocolMessage).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OwnedDeviceDiscoveryProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OwnedDeviceDiscoveryProtocol.java new file mode 100644 index 00000000..ae64470e --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OwnedDeviceDiscoveryProtocol.java @@ -0,0 +1,376 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.protocol.protocols; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import io.olvid.engine.Logger; +import io.olvid.engine.crypto.PRNGService; +import io.olvid.engine.datatypes.DictionaryKey; +import io.olvid.engine.datatypes.EncryptedBytes; +import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.datatypes.UID; +import io.olvid.engine.datatypes.containers.ChannelMessageToSend; +import io.olvid.engine.datatypes.containers.ReceptionChannelInfo; +import io.olvid.engine.datatypes.containers.SendChannelInfo; +import io.olvid.engine.datatypes.containers.ServerQuery; +import io.olvid.engine.encoder.Encoded; +import io.olvid.engine.engine.types.identities.ObvOwnedDevice; +import io.olvid.engine.protocol.databases.ReceivedMessage; +import io.olvid.engine.protocol.datatypes.CoreProtocolMessage; +import io.olvid.engine.protocol.datatypes.ProtocolManagerSession; +import io.olvid.engine.protocol.protocol_engine.ConcreteProtocol; +import io.olvid.engine.protocol.protocol_engine.ConcreteProtocolMessage; +import io.olvid.engine.protocol.protocol_engine.ConcreteProtocolState; +import io.olvid.engine.protocol.protocol_engine.EmptyProtocolMessage; +import io.olvid.engine.protocol.protocol_engine.InitialProtocolState; +import io.olvid.engine.protocol.protocol_engine.ProtocolStep; + +public class OwnedDeviceDiscoveryProtocol extends ConcreteProtocol { + public OwnedDeviceDiscoveryProtocol(ProtocolManagerSession protocolManagerSession, UID protocolInstanceUid, int currentStateId, Encoded encodedCurrentState, Identity ownedIdentity, PRNGService prng, ObjectMapper jsonObjectMapper) throws Exception { + super(protocolManagerSession, protocolInstanceUid, currentStateId, encodedCurrentState, ownedIdentity, prng, jsonObjectMapper); + } + + @Override + public int getProtocolId() { + return OWNED_DEVICE_DISCOVERY_PROTOCOL_ID; + } + + // region States + + public static final int REQUEST_SENT_STATE_ID = 1; + public static final int RESPONSE_PROCESSED_STATE_ID = 2; + public static final int CANCELLED_STATE_ID = 3; + + @Override + public int[] getFinalStateIds() { + return new int[]{CANCELLED_STATE_ID, RESPONSE_PROCESSED_STATE_ID}; + } + + @Override + protected Class getStateClass(int stateId) { + switch (stateId) { + case INITIAL_STATE_ID: + return InitialProtocolState.class; + case REQUEST_SENT_STATE_ID: + return RequestSentState.class; + case RESPONSE_PROCESSED_STATE_ID: + return ResponseProcessedState.class; + case CANCELLED_STATE_ID: + return CancelledState.class; + default: + return null; + } + } + + public static class RequestSentState extends ConcreteProtocolState { + public RequestSentState(Encoded encodedState) throws Exception { + super(REQUEST_SENT_STATE_ID); + Encoded[] list = encodedState.decodeList(); + if (list.length != 0) { + throw new Exception(); + } + } + + public RequestSentState() { + super(REQUEST_SENT_STATE_ID); + } + + @Override + public Encoded encode() { + return Encoded.of(new Encoded[0]); + } + } + + public static class ResponseProcessedState extends ConcreteProtocolState { + public ResponseProcessedState(Encoded encodedState) throws Exception { + super(RESPONSE_PROCESSED_STATE_ID); + Encoded[] list = encodedState.decodeList(); + if (list.length != 0) { + throw new Exception(); + } + } + + public ResponseProcessedState() { + super(RESPONSE_PROCESSED_STATE_ID); + } + + @Override + public Encoded encode() { + return Encoded.of(new Encoded[0]); + } + } + + public static class CancelledState extends ConcreteProtocolState { + public CancelledState(Encoded encodedState) throws Exception { + super(CANCELLED_STATE_ID); + Encoded[] list = encodedState.decodeList(); + if (list.length != 0) { + throw new Exception(); + } + } + + public CancelledState() { + super(CANCELLED_STATE_ID); + } + + @Override + public Encoded encode() { + return Encoded.of(new Encoded[0]); + } + } + + // endregion + + + + // region Messages + + public static final int INITIAL_MESSAGE_ID = 0; + public static final int SERVER_QUERY_MESSAGE_ID = 1; + public static final int TRIGGER_OWNED_DEVICE_DISCOVERY_MESSAGE_ID = 2; + + @Override + protected Class getMessageClass(int protocolMessageId) { + switch (protocolMessageId) { + case INITIAL_MESSAGE_ID: + return InitialMessage.class; + case SERVER_QUERY_MESSAGE_ID: + return ServerQueryMessage.class; + case TRIGGER_OWNED_DEVICE_DISCOVERY_MESSAGE_ID: + return TriggerOwnedDeviceDiscoveryMessage.class; + default: + return null; + } + } + + public static class InitialMessage extends ConcreteProtocolMessage { + public InitialMessage(CoreProtocolMessage coreProtocolMessage) { + super(coreProtocolMessage); + } + + public InitialMessage(ReceivedMessage receivedMessage) throws Exception { + super(new CoreProtocolMessage(receivedMessage)); + if (receivedMessage.getInputs().length != 0) { + throw new Exception(); + } + } + + @Override + public int getProtocolMessageId() { + return INITIAL_MESSAGE_ID; + } + + @Override + public Encoded[] getInputs() { + return new Encoded[0]; + } + } + + + public static class ServerQueryMessage extends ConcreteProtocolMessage { + private final EncryptedBytes encryptedOwnedDeviceList; + + public ServerQueryMessage(CoreProtocolMessage coreProtocolMessage) { + super(coreProtocolMessage); + encryptedOwnedDeviceList = null; + } + + @SuppressWarnings("unused") + public ServerQueryMessage(ReceivedMessage receivedMessage) throws Exception { + super(new CoreProtocolMessage(receivedMessage)); + if (receivedMessage.getEncodedResponse() == null) { + throw new Exception(); + } + encryptedOwnedDeviceList = receivedMessage.getEncodedResponse().decodeEncryptedData(); + } + + @Override + public int getProtocolMessageId() { + return SERVER_QUERY_MESSAGE_ID; + } + + @Override + public Encoded[] getInputs() { + return new Encoded[0]; + } + } + + public static class TriggerOwnedDeviceDiscoveryMessage extends EmptyProtocolMessage { + public TriggerOwnedDeviceDiscoveryMessage(CoreProtocolMessage coreProtocolMessage) { + super(coreProtocolMessage); + } + + public TriggerOwnedDeviceDiscoveryMessage(ReceivedMessage receivedMessage) throws Exception { + super(receivedMessage); + } + + @Override + public int getProtocolMessageId() { + return TRIGGER_OWNED_DEVICE_DISCOVERY_MESSAGE_ID; + } + } + + // endregion + + + + + + // region Steps + + @Override + protected Class[] getPossibleStepClasses(int stateId) { + switch (stateId) { + case INITIAL_STATE_ID: + return new Class[]{SendRequestStep.class}; + case REQUEST_SENT_STATE_ID: + return new Class[]{ProcessResponseStateStep.class}; + case RESPONSE_PROCESSED_STATE_ID: + case CANCELLED_STATE_ID: + default: + return new Class[0]; + } + } + + public static class SendRequestStep extends ProtocolStep { + private final InitialProtocolState startState; + + public SendRequestStep(InitialProtocolState startState, InitialMessage receivedMessage, OwnedDeviceDiscoveryProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createLocalChannelInfo(), receivedMessage, protocol); + this.startState = startState; + } + + public SendRequestStep(InitialProtocolState startState, TriggerOwnedDeviceDiscoveryMessage receivedMessage, OwnedDeviceDiscoveryProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createAnyObliviousChannelWithOwnedDeviceInfo(), receivedMessage, protocol); + this.startState = startState; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createServerQueryChannelInfo(getOwnedIdentity(), ServerQuery.Type.createOwnedDeviceDiscoveryQuery(getOwnedIdentity()))); + ChannelMessageToSend messageToSend = new ServerQueryMessage(coreProtocolMessage).generateChannelServerQueryMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + + return new RequestSentState(); + } + } + + + public static class ProcessResponseStateStep extends ProtocolStep { + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private final RequestSentState startState; + private final ServerQueryMessage receivedMessage; + + public ProcessResponseStateStep(RequestSentState startState, ServerQueryMessage receivedMessage, OwnedDeviceDiscoveryProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createLocalChannelInfo(), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + // decrypt the received device list + byte[] decryptedPayload = protocolManagerSession.encryptionForIdentityDelegate.decrypt(protocolManagerSession.session, receivedMessage.encryptedOwnedDeviceList, getOwnedIdentity()); + if (decryptedPayload == null) { + Logger.w("Unable to DECRYPT received OwnedDeviceDiscoveryProtocol payload (or expired query)!"); + return new CancelledState(); + } + + // we ignore the multi-device boolean received from the server --> it is only used when querying outside the protocol + HashMap serverOwnedDevices = new HashMap<>(); + try { + HashMap map = new Encoded(decryptedPayload).decodeDictionary(); + + Encoded[] encodedDevices = map.get(new DictionaryKey("dev")).decodeList(); + for (Encoded encodedDevice : encodedDevices) { + HashMap deviceMap = encodedDevice.decodeDictionary(); + UID deviceUid = deviceMap.get(new DictionaryKey("uid")).decodeUid(); + + Encoded encodedExpiration = deviceMap.get(new DictionaryKey("exp")); + Long expirationTimestamp = encodedExpiration == null ? null : encodedExpiration.decodeLong(); + + Encoded encodedRegistration = deviceMap.get(new DictionaryKey("reg")); + Long lastRegistrationTimestamp = encodedRegistration == null ? null : encodedRegistration.decodeLong(); + + Encoded encodedName = deviceMap.get(new DictionaryKey("name")); + String deviceName = null; + if (encodedName != null) { + try { + byte[] plaintext = protocolManagerSession.encryptionForIdentityDelegate.decrypt(protocolManagerSession.session, encodedName.decodeEncryptedData(), getOwnedIdentity()); + byte[] bytesDeviceName = new Encoded(plaintext).decodeListWithPadding()[0].decodeBytes(); + if (bytesDeviceName.length != 0) { + deviceName = new String(bytesDeviceName, StandardCharsets.UTF_8); + } + } catch (Exception ignored) { + } + } + + serverOwnedDevices.put(deviceUid, new ObvOwnedDevice.ServerDeviceInfo(deviceName, expirationTimestamp, lastRegistrationTimestamp)); + } + } catch (Exception e) { + Logger.w("Unable to DECODE received OwnedDeviceDiscoveryProtocol payload!"); + return new CancelledState(); + } + + List oldOwnedDevices = protocolManagerSession.identityDelegate.getDevicesOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); + + for (ObvOwnedDevice oldDevice : oldOwnedDevices) { + UID ownedDeviceUid = new UID(oldDevice.bytesDeviceUid); + ObvOwnedDevice.ServerDeviceInfo serverDeviceInfo = serverOwnedDevices.remove(ownedDeviceUid); + if (serverDeviceInfo == null) { + // device was removed from the server + if (oldDevice.currentDevice) { + // our current device was removed! Do not deactivate it yet, but force a registerPushNotification so it gets deactivated if it should be + protocolManagerSession.pushNotificationDelegate.forceRegisterPushNotification(getOwnedIdentity()); + } else { + // a deviceUid was removed --> delete the channel and the deviceUid + protocolManagerSession.channelDelegate.deleteObliviousChannelIfItExists(protocolManagerSession.session, getOwnedIdentity(), ownedDeviceUid, getOwnedIdentity()); + protocolManagerSession.identityDelegate.removeDeviceForOwnedIdentity(protocolManagerSession.session, getOwnedIdentity(), ownedDeviceUid); + } + } else { + // the device exists both locally and on the server --> check what has changed + if (!Objects.equals(oldDevice.serverDeviceInfo, serverDeviceInfo)) { + protocolManagerSession.identityDelegate.updateOwnedDevice(protocolManagerSession.session, getOwnedIdentity(), ownedDeviceUid, serverDeviceInfo.displayName, serverDeviceInfo.expirationTimestamp, serverDeviceInfo.lastRegistrationTimestamp); + } + } + } + + // now create all new server devices locally + for (Map.Entry entry : serverOwnedDevices.entrySet()) { + protocolManagerSession.identityDelegate.addDeviceForOwnedIdentity(protocolManagerSession.session, getOwnedIdentity(), entry.getKey(), entry.getValue().displayName, entry.getValue().expirationTimestamp, entry.getValue().lastRegistrationTimestamp, false); + } + + return new ResponseProcessedState(); + } + } + + // endregion +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OwnedDeviceManagementProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OwnedDeviceManagementProtocol.java new file mode 100644 index 00000000..6bfd098c --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OwnedDeviceManagementProtocol.java @@ -0,0 +1,328 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.protocol.protocols; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.nio.charset.StandardCharsets; + +import io.olvid.engine.Logger; +import io.olvid.engine.crypto.PRNGService; +import io.olvid.engine.crypto.Suite; +import io.olvid.engine.datatypes.EncryptedBytes; +import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.datatypes.NoAcceptableChannelException; +import io.olvid.engine.datatypes.UID; +import io.olvid.engine.datatypes.containers.ChannelMessageToSend; +import io.olvid.engine.datatypes.containers.ReceptionChannelInfo; +import io.olvid.engine.datatypes.containers.SendChannelInfo; +import io.olvid.engine.datatypes.containers.ServerQuery; +import io.olvid.engine.encoder.Encoded; +import io.olvid.engine.engine.types.ObvDeviceManagementRequest; +import io.olvid.engine.protocol.databases.ReceivedMessage; +import io.olvid.engine.protocol.datatypes.CoreProtocolMessage; +import io.olvid.engine.protocol.datatypes.ProtocolManagerSession; +import io.olvid.engine.protocol.protocol_engine.ConcreteProtocol; +import io.olvid.engine.protocol.protocol_engine.ConcreteProtocolMessage; +import io.olvid.engine.protocol.protocol_engine.ConcreteProtocolState; +import io.olvid.engine.protocol.protocol_engine.InitialProtocolState; +import io.olvid.engine.protocol.protocol_engine.ProtocolStep; + +public class OwnedDeviceManagementProtocol extends ConcreteProtocol { + public OwnedDeviceManagementProtocol(ProtocolManagerSession protocolManagerSession, UID protocolInstanceUid, int currentStateId, Encoded encodedCurrentState, Identity ownedIdentity, PRNGService prng, ObjectMapper jsonObjectMapper) throws Exception { + super(protocolManagerSession, protocolInstanceUid, currentStateId, encodedCurrentState, ownedIdentity, prng, jsonObjectMapper); + } + + @Override + public int getProtocolId() { + return OWNED_DEVICE_MANAGEMENT_PROTOCOL_ID; + } + + // region States + + public static final int REQUEST_SENT_STATE_ID = 1; + public static final int RESPONSE_PROCESSED_STATE_ID = 2; + + @Override + public int[] getFinalStateIds() { + return new int[]{RESPONSE_PROCESSED_STATE_ID}; + } + + @Override + protected Class getStateClass(int stateId) { + switch (stateId) { + case INITIAL_STATE_ID: + return InitialProtocolState.class; + case REQUEST_SENT_STATE_ID: + return RequestSentState.class; + case RESPONSE_PROCESSED_STATE_ID: + return ResponseProcessedState.class; + default: + return null; + } + } + + public static class RequestSentState extends ConcreteProtocolState { + public final ObvDeviceManagementRequest deviceManagementRequest; + + @SuppressWarnings("unused") + public RequestSentState(Encoded encodedState) throws Exception { + super(REQUEST_SENT_STATE_ID); + Encoded[] list = encodedState.decodeList(); + if (list.length != 1) { + throw new Exception(); + } + this.deviceManagementRequest = ObvDeviceManagementRequest.of(list[0]); + } + + public RequestSentState(ObvDeviceManagementRequest deviceManagementRequest) { + super(REQUEST_SENT_STATE_ID); + this.deviceManagementRequest = deviceManagementRequest; + } + + @Override + public Encoded encode() { + return Encoded.of(new Encoded[]{ + deviceManagementRequest.encode(), + }); + } + } + + public static class ResponseProcessedState extends ConcreteProtocolState { + public ResponseProcessedState(Encoded encodedState) throws Exception { + super(RESPONSE_PROCESSED_STATE_ID); + Encoded[] list = encodedState.decodeList(); + if (list.length != 0) { + throw new Exception(); + } + } + + public ResponseProcessedState() { + super(RESPONSE_PROCESSED_STATE_ID); + } + + @Override + public Encoded encode() { + return Encoded.of(new Encoded[0]); + } + } + + // endregion + + + + // region Messages + + public static final int INITIAL_MESSAGE_ID = 0; + public static final int SERVER_QUERY_MESSAGE_ID = 1; + + @Override + protected Class getMessageClass(int protocolMessageId) { + switch (protocolMessageId) { + case INITIAL_MESSAGE_ID: + return InitialMessage.class; + case SERVER_QUERY_MESSAGE_ID: + return ServerQueryMessage.class; + default: + return null; + } + } + + public static class InitialMessage extends ConcreteProtocolMessage { + public final ObvDeviceManagementRequest deviceManagementRequest; + + public InitialMessage(CoreProtocolMessage coreProtocolMessage, ObvDeviceManagementRequest deviceManagementRequest) { + super(coreProtocolMessage); + this.deviceManagementRequest = deviceManagementRequest; + } + + public InitialMessage(ReceivedMessage receivedMessage) throws Exception { + super(new CoreProtocolMessage(receivedMessage)); + if (receivedMessage.getInputs().length != 1) { + throw new Exception(); + } + deviceManagementRequest = ObvDeviceManagementRequest.of(receivedMessage.getInputs()[0]); + } + + @Override + public int getProtocolMessageId() { + return INITIAL_MESSAGE_ID; + } + + @Override + public Encoded[] getInputs() { + return new Encoded[]{ + deviceManagementRequest.encode(), + }; + } + } + + + public static class ServerQueryMessage extends ConcreteProtocolMessage { + public ServerQueryMessage(CoreProtocolMessage coreProtocolMessage) { + super(coreProtocolMessage); + } + + @SuppressWarnings("unused") + public ServerQueryMessage(ReceivedMessage receivedMessage) throws Exception { + super(new CoreProtocolMessage(receivedMessage)); + // uncomment if the query has to return a response +// if (receivedMessage.getEncodedResponse() == null) { +// throw new Exception(); +// } + } + + @Override + public int getProtocolMessageId() { + return SERVER_QUERY_MESSAGE_ID; + } + + @Override + public Encoded[] getInputs() { + return new Encoded[0]; + } + } + + // endregion + + + + + + // region Steps + + @Override + protected Class[] getPossibleStepClasses(int stateId) { + switch (stateId) { + case INITIAL_STATE_ID: + return new Class[]{SendRequestStep.class}; + case REQUEST_SENT_STATE_ID: + return new Class[]{ProcessResponseStateStep.class}; + case RESPONSE_PROCESSED_STATE_ID: + default: + return new Class[0]; + } + } + + public static class SendRequestStep extends ProtocolStep { + private final InitialProtocolState startState; + private final InitialMessage receivedMessage; + + public SendRequestStep(InitialProtocolState startState, InitialMessage receivedMessage, OwnedDeviceManagementProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createLocalChannelInfo(), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + ServerQuery.Type serverQueryType; + switch (receivedMessage.deviceManagementRequest.action) { + case ObvDeviceManagementRequest.ACTION_SET_NICKNAME: { + // pad and encrypt the nickname + byte[] encodedDeviceName = Encoded.of(new Encoded[]{ + Encoded.of(receivedMessage.deviceManagementRequest.nickname.getBytes(StandardCharsets.UTF_8)) + }).getBytes(); + + byte[] plaintext = new byte[((encodedDeviceName.length - 1) | 127) + 1]; + System.arraycopy(encodedDeviceName, 0, plaintext, 0, encodedDeviceName.length); + + EncryptedBytes encryptedDeviceName = Suite.getPublicKeyEncryption(getOwnedIdentity().getEncryptionPublicKey()).encrypt( + getOwnedIdentity().getEncryptionPublicKey(), + plaintext, + Suite.getDefaultPRNGService(0)); + + UID currentDeviceUid = protocolManagerSession.identityDelegate.getCurrentDeviceUidOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); + serverQueryType = ServerQuery.Type.createDeviceManagementSetNicknameQuery(getOwnedIdentity(), receivedMessage.deviceManagementRequest.getDeviceUid(), encryptedDeviceName, currentDeviceUid.equals(receivedMessage.deviceManagementRequest.getDeviceUid())); + break; + } + case ObvDeviceManagementRequest.ACTION_DEACTIVATE_DEVICE: { + serverQueryType = ServerQuery.Type.createDeviceManagementDeactivateDeviceQuery(getOwnedIdentity(), receivedMessage.deviceManagementRequest.getDeviceUid()); + break; + } + case ObvDeviceManagementRequest.ACTION_SET_UNEXPIRING_DEVICE: { + serverQueryType = ServerQuery.Type.createDeviceManagementSetUnexpiringDeviceQuery(getOwnedIdentity(), receivedMessage.deviceManagementRequest.getDeviceUid()); + break; + } + default: { + Logger.e("OwnedDeviceManagementProtocol received an invalid ObvDeviceManagementRequest: unknown action"); + throw new Exception(); + } + } + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createServerQueryChannelInfo(getOwnedIdentity(), serverQueryType)); + ChannelMessageToSend messageToSend = new ServerQueryMessage(coreProtocolMessage).generateChannelServerQueryMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + + return new RequestSentState(receivedMessage.deviceManagementRequest); + } + } + + + public static class ProcessResponseStateStep extends ProtocolStep { + @SuppressWarnings({"unused", "FieldCanBeLocal"}) + private final RequestSentState startState; + private final ServerQueryMessage receivedMessage; + + public ProcessResponseStateStep(RequestSentState startState, ServerQueryMessage receivedMessage, OwnedDeviceManagementProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createLocalChannelInfo(), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + { + // after a the query is processed by the server, start an OwnedDeviceDiscoveryProtocol + UID protocolInstanceUid = new UID(getPrng()); + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), + ConcreteProtocol.OWNED_DEVICE_DISCOVERY_PROTOCOL_ID, + protocolInstanceUid, + false); + ChannelMessageToSend message = new OwnedDeviceDiscoveryProtocol.InitialMessage(coreProtocolMessage).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message, getPrng()); + } + + // if the user deactivated a device --> notify all contacts that a device discovery is needed + if (startState.deviceManagementRequest.action == ObvDeviceManagementRequest.ACTION_DEACTIVATE_DEVICE) { + SendChannelInfo[] sendChannelInfos = SendChannelInfo.createAllConfirmedObliviousChannelsInfosForMultipleIdentities(protocolManagerSession.identityDelegate.getContactsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()), getOwnedIdentity()); + for (SendChannelInfo sendChannelInfo : sendChannelInfos) { + try { + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(sendChannelInfo, + ConcreteProtocol.CONTACT_MANAGEMENT_PROTOCOL_ID, + new UID(getPrng()), + false); + ChannelMessageToSend message = new ContactManagementProtocol.PerformContactDeviceDiscoveryMessage(coreProtocolMessage).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message, getPrng()); + } catch (NoAcceptableChannelException e) { + Logger.d("One SendChannelInfo with no channel during OwnedDeviceManagementProtocol.ProcessResponseStateStep"); + } + } + } + + return new ResponseProcessedState(); + } + } + + // endregion +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OwnedIdentityDeletionProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OwnedIdentityDeletionProtocol.java new file mode 100644 index 00000000..49c221e5 --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OwnedIdentityDeletionProtocol.java @@ -0,0 +1,767 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.protocol.protocols; + + +import static io.olvid.engine.protocol.protocols.TrustEstablishmentWithMutualScanProtocol.FINISHED_STATE_ID; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import io.olvid.engine.Logger; +import io.olvid.engine.crypto.KDF; +import io.olvid.engine.crypto.PRNGService; +import io.olvid.engine.crypto.Signature; +import io.olvid.engine.crypto.Suite; +import io.olvid.engine.datatypes.Constants; +import io.olvid.engine.datatypes.EncryptedBytes; +import io.olvid.engine.datatypes.GroupMembersChangedCallback; +import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.datatypes.NoAcceptableChannelException; +import io.olvid.engine.datatypes.UID; +import io.olvid.engine.datatypes.containers.ChannelMessageToSend; +import io.olvid.engine.datatypes.containers.Group; +import io.olvid.engine.datatypes.containers.GroupInformation; +import io.olvid.engine.datatypes.containers.GroupV2; +import io.olvid.engine.datatypes.containers.ReceptionChannelInfo; +import io.olvid.engine.datatypes.containers.SendChannelInfo; +import io.olvid.engine.datatypes.containers.ServerQuery; +import io.olvid.engine.datatypes.key.symmetric.AuthEncKey; +import io.olvid.engine.datatypes.notifications.ProtocolNotifications; +import io.olvid.engine.encoder.Encoded; +import io.olvid.engine.engine.types.identities.ObvGroupV2; +import io.olvid.engine.protocol.databases.IdentityDeletionSignatureReceived; +import io.olvid.engine.protocol.databases.ReceivedMessage; +import io.olvid.engine.protocol.datatypes.CoreProtocolMessage; +import io.olvid.engine.protocol.datatypes.ProtocolManagerSession; +import io.olvid.engine.protocol.protocol_engine.ConcreteProtocol; +import io.olvid.engine.protocol.protocol_engine.ConcreteProtocolMessage; +import io.olvid.engine.protocol.protocol_engine.ConcreteProtocolState; +import io.olvid.engine.protocol.protocol_engine.EmptyProtocolMessage; +import io.olvid.engine.protocol.protocol_engine.InitialProtocolState; +import io.olvid.engine.protocol.protocol_engine.ProtocolStep; + +public class OwnedIdentityDeletionProtocol extends ConcreteProtocol { + public OwnedIdentityDeletionProtocol(ProtocolManagerSession protocolManagerSession, UID protocolInstanceUid, int currentStateId, Encoded encodedCurrentState, Identity ownedIdentity, PRNGService prng, ObjectMapper jsonObjectMapper) throws Exception { + super(protocolManagerSession, protocolInstanceUid, currentStateId, encodedCurrentState, ownedIdentity, prng, jsonObjectMapper); + } + + + @Override + public int getProtocolId() { + return OWNED_IDENTITY_DELETION_PROTOCOL_ID; + } + + // region states + + public static final int FINISHED_STATED_ID = 1; + public static final int UNREGISTERING_FROM_SERVER_STATE_ID = 2; + + @Override + public int[] getFinalStateIds() { + return new int[]{FINISHED_STATED_ID}; + } + + + @Override + protected Class getStateClass(int stateId) { + switch (stateId) { + case INITIAL_STATE_ID: + return InitialProtocolState.class; + case FINISHED_STATED_ID: + return FinishedProtocolState.class; + case UNREGISTERING_FROM_SERVER_STATE_ID: + return UnregisteringFromServerState.class; + default: + return null; + } + } + + + public static class FinishedProtocolState extends ConcreteProtocolState { + public FinishedProtocolState() { + super(FINISHED_STATE_ID); + } + + @SuppressWarnings({"unused", "RedundantSuppression"}) + public FinishedProtocolState(Encoded encodedState) throws Exception { + super(FINISHED_STATE_ID); + Encoded[] list = encodedState.decodeList(); + if (list.length != 0) { + throw new Exception(); + } + } + + @Override + public Encoded encode() { + return Encoded.of(new Encoded[0]); + } + } + + + + public static class UnregisteringFromServerState extends ConcreteProtocolState { + private final boolean deleteEverywhere; + private final boolean propagated; + public UnregisteringFromServerState(boolean deleteEverywhere, boolean propagated) { + super(UNREGISTERING_FROM_SERVER_STATE_ID); + this.deleteEverywhere = deleteEverywhere; + this.propagated = propagated; + } + + @SuppressWarnings({"unused", "RedundantSuppression"}) + public UnregisteringFromServerState(Encoded encodedState) throws Exception { + super(UNREGISTERING_FROM_SERVER_STATE_ID); + Encoded[] list = encodedState.decodeList(); + if (list.length != 2) { + throw new Exception(); + } + this.deleteEverywhere = list[0].decodeBoolean(); + this.propagated = list[1].decodeBoolean(); + } + + @Override + public Encoded encode() { + return Encoded.of(new Encoded[]{ + Encoded.of(deleteEverywhere), + Encoded.of(propagated), + }); + } + } + + // endregion + + + + + + + + // region messages + static final int INITIAL_MESSAGE_ID = 0; + static final int CONTACT_OWNED_IDENTITY_WAS_DELETED_MESSAGE_ID = 1; + static final int PROPAGATE_OWNED_IDENTITY_DELETED_MESSAGE_ID = 2; + static final int SKIP_SERVER_QUERY_MESSAGE_ID = 3; + static final int DEACTIVATE_CURRENT_DEVICE_SERVER_QUERY_MESSAGE_ID = 4; + + + @Override + protected Class getMessageClass(int protocolMessageId) { + switch (protocolMessageId) { + case INITIAL_MESSAGE_ID: + return InitialMessage.class; + case CONTACT_OWNED_IDENTITY_WAS_DELETED_MESSAGE_ID: + return ContactOwnedIdentityWasDeletedMessage.class; + case PROPAGATE_OWNED_IDENTITY_DELETED_MESSAGE_ID: + return PropagateOwnedIdentityDeletedMessage.class; + case SKIP_SERVER_QUERY_MESSAGE_ID: + return SkipServerQueryMessage.class; + case DEACTIVATE_CURRENT_DEVICE_SERVER_QUERY_MESSAGE_ID: + return DeactivateCurrentDeviceServerQueryMessage.class; + default: + return null; + } + } + + public static class InitialMessage extends ConcreteProtocolMessage { + protected final boolean deleteEverywhere; + public InitialMessage(CoreProtocolMessage coreProtocolMessage, boolean deleteEverywhere) { + super(coreProtocolMessage); + this.deleteEverywhere = deleteEverywhere; + } + + @SuppressWarnings({"unused", "RedundantSuppression"}) + public InitialMessage(ReceivedMessage receivedMessage) throws Exception { + super(new CoreProtocolMessage(receivedMessage)); + if (receivedMessage.getInputs().length != 1) { + throw new Exception(); + } + this.deleteEverywhere = receivedMessage.getInputs()[0].decodeBoolean(); + } + + @Override + public int getProtocolMessageId() { + return INITIAL_MESSAGE_ID; + } + + @Override + public Encoded[] getInputs() { + return new Encoded[] { + Encoded.of(deleteEverywhere) + }; + } + } + + public static class ContactOwnedIdentityWasDeletedMessage extends ConcreteProtocolMessage { + private final Identity deletedContactOwnedIdentity; + private final byte[] signature; + + public ContactOwnedIdentityWasDeletedMessage(CoreProtocolMessage coreProtocolMessage, Identity deletedContactOwnedIdentity, byte[] signature) { + super(coreProtocolMessage); + this.deletedContactOwnedIdentity = deletedContactOwnedIdentity; + this.signature = signature; + } + + @SuppressWarnings({"unused", "RedundantSuppression"}) + public ContactOwnedIdentityWasDeletedMessage(ReceivedMessage receivedMessage) throws Exception { + super(new CoreProtocolMessage(receivedMessage)); + Encoded[] inputs = receivedMessage.getInputs(); + if (inputs.length != 2) { + throw new Exception(); + } + this.deletedContactOwnedIdentity = inputs[0].decodeIdentity(); + this.signature = inputs[1].decodeBytes(); + } + + @Override + public int getProtocolMessageId() { + return CONTACT_OWNED_IDENTITY_WAS_DELETED_MESSAGE_ID; + } + + @Override + public Encoded[] getInputs() { + return new Encoded[]{ + Encoded.of(deletedContactOwnedIdentity), + Encoded.of(signature), + }; + } + } + + public static class PropagateOwnedIdentityDeletedMessage extends EmptyProtocolMessage { + public PropagateOwnedIdentityDeletedMessage(CoreProtocolMessage coreProtocolMessage) { + super(coreProtocolMessage); + } + + @SuppressWarnings("unused") + public PropagateOwnedIdentityDeletedMessage(ReceivedMessage receivedMessage) throws Exception { + super(receivedMessage); + } + + @Override + public int getProtocolMessageId() { + return PROPAGATE_OWNED_IDENTITY_DELETED_MESSAGE_ID; + } + } + + public static class SkipServerQueryMessage extends EmptyProtocolMessage { + public SkipServerQueryMessage(CoreProtocolMessage coreProtocolMessage) { + super(coreProtocolMessage); + } + + @SuppressWarnings("unused") + public SkipServerQueryMessage(ReceivedMessage receivedMessage) throws Exception { + super(receivedMessage); + } + + @Override + public int getProtocolMessageId() { + return SKIP_SERVER_QUERY_MESSAGE_ID; + } + } + + public static class DeactivateCurrentDeviceServerQueryMessage extends EmptyProtocolMessage { + public DeactivateCurrentDeviceServerQueryMessage(CoreProtocolMessage coreProtocolMessage) { + super(coreProtocolMessage); + } + + @SuppressWarnings("unused") + public DeactivateCurrentDeviceServerQueryMessage(ReceivedMessage receivedMessage) throws Exception { + super(receivedMessage); + } + + @Override + public int getProtocolMessageId() { + return DEACTIVATE_CURRENT_DEVICE_SERVER_QUERY_MESSAGE_ID; + } + } + + // endregion + + + + + + + // region steps + @Override + protected Class[] getPossibleStepClasses(int stateId) { + switch (stateId) { + case INITIAL_STATE_ID: + return new Class[]{StartDeletionStep.class, ProcessContactOwnedIdentityWasDeletedMessageStep.class}; + case UNREGISTERING_FROM_SERVER_STATE_ID: + return new Class[]{FinalizeDeletionStep.class}; + case FINISHED_STATED_ID: + default: + return new Class[0]; + } + } + + public static class StartDeletionStep extends ProtocolStep { + InitialProtocolState startState; + boolean deleteEverywhere; + boolean propagated; + + @SuppressWarnings("unused") + public StartDeletionStep(InitialProtocolState startState, InitialMessage receivedMessage, OwnedIdentityDeletionProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createLocalChannelInfo(), receivedMessage, protocol); + this.startState = startState; + this.deleteEverywhere = receivedMessage.deleteEverywhere; + this.propagated = false; + } + + @SuppressWarnings("unused") + public StartDeletionStep(InitialProtocolState startState, PropagateOwnedIdentityDeletedMessage receivedMessage, OwnedIdentityDeletionProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createAnyObliviousChannelWithOwnedDeviceInfo(), receivedMessage, protocol); + this.startState = startState; + this.deleteEverywhere = true; + this.propagated = true; + } + + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + boolean ownedIdentityIsActive = protocolManagerSession.identityDelegate.isActiveOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); + + if (!ownedIdentityIsActive && !propagated && deleteEverywhere) { + Logger.e("Error: running OwnedIdentityDeletionProtocol.StartDeletionStep with deleteEverywhere and an inactive identity."); + throw new Exception(); + } + + //////////// + // before anything, cleanup inbox, outbox, and protocols. This MUST be done before sending notifications! + protocolManagerSession.engineOwnedIdentityCleanupDelegate.deleteOwnedIdentityFromInboxOutboxProtocolsAndDialogs(protocolManagerSession.session, getOwnedIdentity(), getProtocolInstanceUid()); + + //////////// + // also mark the owned identity for deletion so it is not recreated on app side + protocolManagerSession.identityDelegate.markOwnedIdentityForDeletion(protocolManagerSession.session, getOwnedIdentity()); + + + if (ownedIdentityIsActive) { + //////////// + // delete current device from server + { + UID currentDeviceUid = protocolManagerSession.identityDelegate.getCurrentDeviceUidOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); + + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createServerQueryChannelInfo(getOwnedIdentity(), ServerQuery.Type.createDeviceManagementDeactivateDeviceQuery(getOwnedIdentity(), currentDeviceUid))); + ChannelMessageToSend messageToSend = new DeactivateCurrentDeviceServerQueryMessage(coreProtocolMessage).generateChannelServerQueryMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } + + + if (!propagated && deleteEverywhere) { + /////////// + // if in deleteEverywhere mode, propagate to other owned devices + { + int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; + if (numberOfOtherDevices > 0) { + try { + // send an owned identity deletion propagation message + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateOwnedIdentityDeletedMessage(coreProtocolMessage).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { + } + } + } + } + } else { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new SkipServerQueryMessage(coreProtocolMessage).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } + + return new UnregisteringFromServerState(deleteEverywhere, propagated); + } + } + + + public static class FinalizeDeletionStep extends ProtocolStep { + UnregisteringFromServerState startState; + + @SuppressWarnings("unused") + public FinalizeDeletionStep(UnregisteringFromServerState startState, DeactivateCurrentDeviceServerQueryMessage receivedMessage, OwnedIdentityDeletionProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createLocalChannelInfo(), receivedMessage, protocol); + this.startState = startState; + } + + @SuppressWarnings("unused") + public FinalizeDeletionStep(UnregisteringFromServerState startState, SkipServerQueryMessage receivedMessage, OwnedIdentityDeletionProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createLocalChannelInfo(), receivedMessage, protocol); + this.startState = startState; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + boolean ownedIdentityIsActive = protocolManagerSession.identityDelegate.isActiveOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); + + + /////////// + // if not in deleteEverywhere mode, notify other owned devices to do a device discovery + if (ownedIdentityIsActive && !startState.propagated && !startState.deleteEverywhere) { + int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; + if (numberOfOtherDevices > 0) { + try { + // trigger a device discovery on other devices + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity()), + OWNED_DEVICE_DISCOVERY_PROTOCOL_ID, + new UID(getPrng()), + false); + ChannelMessageToSend messageToSend = new OwnedDeviceDiscoveryProtocol.TriggerOwnedDeviceDiscoveryMessage(coreProtocolMessage).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { + } + } + } + + //////////// + // send delete notifications to contacts + if (ownedIdentityIsActive && !startState.propagated) { + Identity[] contactIdentities = protocolManagerSession.identityDelegate.getContactsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); + if (contactIdentities.length > 0) { + if (startState.deleteEverywhere) { + for (Identity contactIdentity : contactIdentities) { + try { + byte[] signature = protocolManagerSession.identityDelegate.signBlock(protocolManagerSession.session, Constants.SignatureContext.OWNED_IDENTITY_DELETION, contactIdentity.getBytes(), getOwnedIdentity(), getPrng()); + + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAsymmetricBroadcastChannelInfo(contactIdentity, getOwnedIdentity())); + ChannelMessageToSend messageToSend = new ContactOwnedIdentityWasDeletedMessage(coreProtocolMessage, getOwnedIdentity(), signature).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (Exception ignored) { + // continue even if there is an exception, contact notification is only best effort! + } + } + + // We no longer send the "legacy" delete contact message as it may mess up the treatment of our new ContactOwnedIdentityWasDeletedMessage + } else { + // if not a global deletion, simply trigger a device discovery on contact side + SendChannelInfo[] sendChannelInfos = SendChannelInfo.createAllConfirmedObliviousChannelsInfosForMultipleIdentities(contactIdentities, getOwnedIdentity()); + for (SendChannelInfo sendChannelInfo : sendChannelInfos) { + try { + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(sendChannelInfo, + CONTACT_MANAGEMENT_PROTOCOL_ID, + new UID(getPrng()), + false); + ChannelMessageToSend messageToSend = new ContactManagementProtocol.PerformContactDeviceDiscoveryMessage(coreProtocolMessage).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (Exception ignored) { + // continue even if there is an exception, contact notification is only best effort! + } + } + } + } + } + + + //////////// + // only actually disband/leave groups if appropriate + //////////// + if (ownedIdentityIsActive && !startState.propagated && startState.deleteEverywhere) { + + //////////// + // disband all owned groups & leave all joined groups + { + Group[] groups = protocolManagerSession.identityDelegate.getGroupsForOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); + for (Group group : groups) { + GroupInformation groupInformation = protocolManagerSession.identityDelegate.getGroupInformation(protocolManagerSession.session, getOwnedIdentity(), group.getGroupOwnerAndUid()); + UID protocolInstanceUid = groupInformation.computeProtocolUid(); + + if (group.getGroupOwner() == null) { + //////////// + // owned group -> kick all members and pending members + if (group.getGroupMembers().length > 0) { + SendChannelInfo[] sendChannelInfos = SendChannelInfo.createAllConfirmedObliviousChannelsInfosForMultipleIdentities(group.getGroupMembers(), getOwnedIdentity()); + for (SendChannelInfo sendChannelInfo : sendChannelInfos) { + try { + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(sendChannelInfo, + GROUP_MANAGEMENT_PROTOCOL_ID, + protocolInstanceUid, + false); + ChannelMessageToSend messageToSend = new GroupManagementProtocol.KickFromGroupMessage(coreProtocolMessage, groupInformation).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (Exception ignored) { + // continue even if there is an exception, contact notification is only best effort! + } + } + } + if (group.getPendingGroupMembers().length > 0) { + Identity[] pendingMemberIdentities = new Identity[group.getPendingGroupMembers().length]; + for (int i = 0; i < pendingMemberIdentities.length; i++) { + pendingMemberIdentities[i] = group.getPendingGroupMembers()[i].identity; + } + + SendChannelInfo[] sendChannelInfos = SendChannelInfo.createAllConfirmedObliviousChannelsInfosForMultipleIdentities(pendingMemberIdentities, getOwnedIdentity()); + for (SendChannelInfo sendChannelInfo : sendChannelInfos) { + try { + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(sendChannelInfo, + GROUP_MANAGEMENT_PROTOCOL_ID, + protocolInstanceUid, + false); + ChannelMessageToSend messageToSend = new GroupManagementProtocol.KickFromGroupMessage(coreProtocolMessage, groupInformation).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (Exception ignored) { + // continue even if there is an exception, contact notification is only best effort! + } + } + } + } else { + //////////// + // joined group -> notify group owner + try { + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createAllConfirmedObliviousChannelsInfo(group.getGroupOwner(), getOwnedIdentity()), + GROUP_MANAGEMENT_PROTOCOL_ID, + protocolInstanceUid, + false); + ChannelMessageToSend message = new GroupManagementProtocol.NotifyGroupLeftMessage(coreProtocolMessage, groupInformation).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message, getPrng()); + } catch (Exception ignored) { + // continue even if there is an exception, contact notification is only best effort! + } + } + } + } + + //////////// + // leave all groups v2 & disband those where I am the only admin + { + List groupsV2 = protocolManagerSession.identityDelegate.getObvGroupsV2ForOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); + for (ObvGroupV2 groupV2 : groupsV2) { + if (groupV2.groupIdentifier.category == GroupV2.Identifier.CATEGORY_SERVER) { + // only consider non-keycloak groups + try { + // check if I am the only non-pending admin of this group + boolean iAmTheOnlyAdmin; + if (groupV2.ownPermissions.contains(GroupV2.Permission.GROUP_ADMIN)) { + iAmTheOnlyAdmin = true; + for (ObvGroupV2.ObvGroupV2Member member : groupV2.otherGroupMembers) { + if (member.permissions.contains(GroupV2.Permission.GROUP_ADMIN)) { + iAmTheOnlyAdmin = false; + break; + } + } + } else { + iAmTheOnlyAdmin = false; + } + + if (iAmTheOnlyAdmin) { + // delete the blob on the server + GroupV2.BlobKeys blobKeys = protocolManagerSession.identityDelegate.getGroupV2BlobKeys(protocolManagerSession.session, getOwnedIdentity(), groupV2.groupIdentifier); + { + byte[] signature = Signature.sign(Constants.SignatureContext.GROUP_DELETE_ON_SERVER, blobKeys.groupAdminServerAuthenticationPrivateKey.getSignaturePrivateKey(), getPrng()); + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createServerQueryChannelInfo(getOwnedIdentity(), ServerQuery.Type.createDeleteGroupBlobQuery(groupV2.groupIdentifier, signature)), ConcreteProtocol.GROUPS_V2_PROTOCOL_ID, new UID(getPrng()), false); + ChannelMessageToSend messageToSend = new GroupsV2Protocol.DeleteGroupBlobFromServerMessage(coreProtocolMessage).generateChannelServerQueryMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } + + // immediately kick all members + byte[] chainPlaintext = protocolManagerSession.identityDelegate.getGroupV2AdministratorsChain(protocolManagerSession.session, getOwnedIdentity(), groupV2.groupIdentifier).encode().getBytes(); + AuthEncKey encryptionKey = (AuthEncKey) Suite.getKDF(KDF.KDF_SHA256).gen(blobKeys.blobMainSeed, Suite.getDefaultAuthEnc(0).getKDFDelegate())[0]; + EncryptedBytes encryptedChain = Suite.getAuthEnc(encryptionKey).encrypt(encryptionKey, chainPlaintext, getPrng()); + + GroupV2.ServerBlob serverBlob = protocolManagerSession.identityDelegate.getGroupV2ServerBlob(protocolManagerSession.session, getOwnedIdentity(), groupV2.groupIdentifier); + + for (GroupV2.IdentityAndPermissionsAndDetails member : serverBlob.groupMemberIdentityAndPermissionsAndDetailsList) { + if (member.identity.equals(getOwnedIdentity())) { + continue; + } + + byte[] dataToSign = new byte[encryptedChain.length + member.groupInvitationNonce.length]; + System.arraycopy(encryptedChain.getBytes(), 0, dataToSign, 0, encryptedChain.length); + System.arraycopy(member.groupInvitationNonce, 0, dataToSign, encryptedChain.length, member.groupInvitationNonce.length); + + byte[] signature = protocolManagerSession.identityDelegate.signBlock(protocolManagerSession.session, Constants.SignatureContext.GROUP_KICK, dataToSign, getOwnedIdentity(), getPrng()); + + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createAsymmetricBroadcastChannelInfo(member.identity, getOwnedIdentity()), ConcreteProtocol.GROUPS_V2_PROTOCOL_ID, groupV2.groupIdentifier.computeProtocolInstanceUid(), false); + ChannelMessageToSend messageToSend = new GroupsV2Protocol.KickMessage(coreProtocolMessage, groupV2.groupIdentifier, encryptedChain, signature).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } + } else { + byte[] ownGroupInvitationNonce = protocolManagerSession.identityDelegate.getGroupV2OwnGroupInvitationNonce(protocolManagerSession.session, getOwnedIdentity(), groupV2.groupIdentifier); + if (ownGroupInvitationNonce != null) { + // put a group left log on server + // we do not notify the group members: they will refresh the groups when we send them the contact deletion message + byte[] leaveSignature = protocolManagerSession.identityDelegate.signGroupInvitationNonce( + protocolManagerSession.session, + Constants.SignatureContext.GROUP_LEAVE_NONCE, + groupV2.groupIdentifier, + ownGroupInvitationNonce, + null, + getOwnedIdentity(), + getPrng()); + + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createServerQueryChannelInfo(getOwnedIdentity(), ServerQuery.Type.createPutGroupLogQuery(groupV2.groupIdentifier, leaveSignature)), ConcreteProtocol.GROUPS_V2_PROTOCOL_ID, new UID(getPrng()), false); + ChannelMessageToSend messageToSend = new GroupsV2Protocol.PutGroupLogOnServerMessage(coreProtocolMessage).generateChannelServerQueryMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } + } + } catch (Exception ignored) { + // continue even if there is an exception, contact notification is only best effort! + } + } + } + } + } + + + // finally, delete the server session, all channels (all notifications message have already been encrypted) and actually delete owned identity + protocolManagerSession.engineOwnedIdentityCleanupDelegate.deleteOwnedIdentityServerSession(protocolManagerSession.session, getOwnedIdentity()); + protocolManagerSession.channelDelegate.deleteAllChannelsForOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); + protocolManagerSession.identityDelegate.deleteOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); + + if (startState.propagated) { + protocolManagerSession.session.addSessionCommitListener(() -> { + HashMap userInfo = new HashMap<>(); + userInfo.put(ProtocolNotifications.NOTIFICATION_OWNED_IDENTITY_DELETED_FROM_ANOTHER_DEVICE_OWNED_IDENTITY_KEY, getOwnedIdentity()); + protocolManagerSession.notificationPostingDelegate.postNotification(ProtocolNotifications.NOTIFICATION_OWNED_IDENTITY_DELETED_FROM_ANOTHER_DEVICE, userInfo); + }); + } + + return new FinishedProtocolState(); + } + } + + + public static class ProcessContactOwnedIdentityWasDeletedMessageStep extends ProtocolStep { + InitialProtocolState startState; + ContactOwnedIdentityWasDeletedMessage receivedMessage; + boolean propagated; + + public ProcessContactOwnedIdentityWasDeletedMessageStep(InitialProtocolState startState, ContactOwnedIdentityWasDeletedMessage receivedMessage, OwnedIdentityDeletionProtocol protocol) throws Exception { + super((receivedMessage.getReceptionChannelInfo().getChannelType() == ReceptionChannelInfo.ASYMMETRIC_CHANNEL_TYPE) ? ReceptionChannelInfo.createAsymmetricChannelInfo() : ReceptionChannelInfo.createAnyObliviousChannelWithOwnedDeviceInfo(), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + propagated = receivedMessage.getReceptionChannelInfo().getChannelType() != ReceptionChannelInfo.ASYMMETRIC_CHANNEL_TYPE; + } + + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + { + // check the message is not a replay + if (IdentityDeletionSignatureReceived.exists(protocolManagerSession, getOwnedIdentity(), receivedMessage.signature)) { + Logger.w("Received a ContactOwnedIdentityWasDeletedMessage with a known signature"); + return new FinishedProtocolState(); + } + } + + { + // verify the signature + if (!Signature.verify(Constants.SignatureContext.OWNED_IDENTITY_DELETION, getOwnedIdentity().getBytes(), receivedMessage.deletedContactOwnedIdentity, receivedMessage.signature)) { + Logger.w("Received a ContactOwnedIdentityWasDeletedMessage with an invalid signature"); + return new FinishedProtocolState(); + } + } + + // save the signature to prevent replay + IdentityDeletionSignatureReceived.create(protocolManagerSession, getOwnedIdentity(), receivedMessage.signature); + + if (!propagated) { + // propagate the message to other owned devices + + int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; + if (numberOfOtherDevices > 0) { + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new ContactOwnedIdentityWasDeletedMessage(coreProtocolMessage, receivedMessage.deletedContactOwnedIdentity, receivedMessage.signature).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } + } + } + + + // now we can delete everything related to this contact + + { + // delete all channels + protocolManagerSession.channelDelegate.deleteObliviousChannelsWithContact(protocolManagerSession.session, getOwnedIdentity(), receivedMessage.deletedContactOwnedIdentity); + } + + { + // deal with group v1 + List groupOwnerAndUids = new ArrayList<>(Arrays.asList(protocolManagerSession.identityDelegate.getGroupOwnerAndUidOfGroupsWhereContactIsPending(protocolManagerSession.session, receivedMessage.deletedContactOwnedIdentity, getOwnedIdentity()))); + groupOwnerAndUids.addAll(Arrays.asList(protocolManagerSession.identityDelegate.getGroupOwnerAndUidsOfGroupsContainingContact(protocolManagerSession.session, receivedMessage.deletedContactOwnedIdentity, getOwnedIdentity()))); + + for (byte[] groupOwnerAndUid : groupOwnerAndUids) { + Group group = protocolManagerSession.identityDelegate.getGroup(protocolManagerSession.session, getOwnedIdentity(), groupOwnerAndUid); + if (!propagated && group.getGroupOwner() == null) { + // I own the group --> properly remove the member from the group and trigger the step to notify others + GroupInformation groupInformation = protocolManagerSession.identityDelegate.getGroupInformation(protocolManagerSession.session, getOwnedIdentity(), groupOwnerAndUid); + + GroupMembersChangedCallback groupMembersChangedCallback = () -> { + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), ConcreteProtocol.GROUP_MANAGEMENT_PROTOCOL_ID, groupInformation.computeProtocolUid(), false); + ChannelMessageToSend messageToSend = new GroupManagementProtocol.GroupMembersOrDetailsChangedTriggerMessage(coreProtocolMessage, groupInformation).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + }; + + protocolManagerSession.identityDelegate.removeMembersAndPendingFromGroup(protocolManagerSession.session, groupOwnerAndUid, getOwnedIdentity(), new Identity[]{receivedMessage.deletedContactOwnedIdentity}, groupMembersChangedCallback); + } else { + // I joined the group (or it is propagated) + if (receivedMessage.deletedContactOwnedIdentity.equals(group.getGroupOwner())) { + // the removed contact was the group owner --> leave the group + protocolManagerSession.identityDelegate.leaveGroup(protocolManagerSession.session, groupOwnerAndUid, getOwnedIdentity()); + } else { + // remove the member/pending member before receiving the notification from the group owner + protocolManagerSession.identityDelegate.forcefullyRemoveMemberOrPendingFromJoinedGroup(protocolManagerSession.session, getOwnedIdentity(), groupOwnerAndUid, receivedMessage.deletedContactOwnedIdentity); + } + } + } + } + + + { + // deal with group v2 + for (GroupV2.IdentifierAndAdminStatus identifierAndAdminStatus : protocolManagerSession.identityDelegate.getServerGroupsV2IdentifierAndMyAdminStatusForContact(protocolManagerSession.session, getOwnedIdentity(), receivedMessage.deletedContactOwnedIdentity)) { + if (!propagated && identifierAndAdminStatus.iAmAdmin) { + // I am a group admin --> start the standard group update protocol + ObvGroupV2.ObvGroupV2ChangeSet changeSet = new ObvGroupV2.ObvGroupV2ChangeSet(); + changeSet.removedMembers.add(receivedMessage.deletedContactOwnedIdentity.getBytes()); + + CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), + ConcreteProtocol.GROUPS_V2_PROTOCOL_ID, + identifierAndAdminStatus.groupIdentifier.computeProtocolInstanceUid(), + false); + + ChannelMessageToSend messageToSend = new GroupsV2Protocol.GroupUpdateInitialMessage(coreProtocolMessage, identifierAndAdminStatus.groupIdentifier, changeSet).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } + + // always remove contact from the group immediately: if admin, this does not prevent the update to work, if not, we will get an update/disband message soon + protocolManagerSession.identityDelegate.forcefullyRemoveMemberOrPendingFromNonAdminGroupV2(protocolManagerSession.session, getOwnedIdentity(), identifierAndAdminStatus.groupIdentifier, receivedMessage.deletedContactOwnedIdentity); + } + } + + // delete contact, do not fail if there are still some groups (typically, groups v2 where I am admin) + protocolManagerSession.identityDelegate.deleteContactIdentity(protocolManagerSession.session, getOwnedIdentity(), receivedMessage.deletedContactOwnedIdentity, false); + + return new FinishedProtocolState(); + } + } + + // endregion +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OwnedIdentityDeletionWithContactNotificationProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OwnedIdentityDeletionWithContactNotificationProtocol.java deleted file mode 100644 index 5533e16e..00000000 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/OwnedIdentityDeletionWithContactNotificationProtocol.java +++ /dev/null @@ -1,521 +0,0 @@ -/* - * Olvid for Android - * Copyright © 2019-2023 Olvid SAS - * - * This file is part of Olvid for Android. - * - * Olvid is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * Olvid is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Olvid. If not, see . - */ - -package io.olvid.engine.protocol.protocols; - - -import static io.olvid.engine.protocol.protocols.TrustEstablishmentWithMutualScanProtocol.FINISHED_STATE_ID; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import io.olvid.engine.Logger; -import io.olvid.engine.crypto.KDF; -import io.olvid.engine.crypto.PRNGService; -import io.olvid.engine.crypto.Signature; -import io.olvid.engine.crypto.Suite; -import io.olvid.engine.datatypes.Constants; -import io.olvid.engine.datatypes.EncryptedBytes; -import io.olvid.engine.datatypes.GroupMembersChangedCallback; -import io.olvid.engine.datatypes.Identity; -import io.olvid.engine.datatypes.UID; -import io.olvid.engine.datatypes.containers.ChannelMessageToSend; -import io.olvid.engine.datatypes.containers.Group; -import io.olvid.engine.datatypes.containers.GroupInformation; -import io.olvid.engine.datatypes.containers.GroupV2; -import io.olvid.engine.datatypes.containers.ReceptionChannelInfo; -import io.olvid.engine.datatypes.containers.SendChannelInfo; -import io.olvid.engine.datatypes.containers.ServerQuery; -import io.olvid.engine.datatypes.key.symmetric.AuthEncKey; -import io.olvid.engine.encoder.Encoded; -import io.olvid.engine.engine.types.identities.ObvGroupV2; -import io.olvid.engine.protocol.databases.IdentityDeletionSignatureReceived; -import io.olvid.engine.protocol.databases.ReceivedMessage; -import io.olvid.engine.protocol.datatypes.CoreProtocolMessage; -import io.olvid.engine.protocol.datatypes.ProtocolManagerSession; -import io.olvid.engine.protocol.protocol_engine.ConcreteProtocol; -import io.olvid.engine.protocol.protocol_engine.ConcreteProtocolMessage; -import io.olvid.engine.protocol.protocol_engine.ConcreteProtocolState; -import io.olvid.engine.protocol.protocol_engine.InitialProtocolState; -import io.olvid.engine.protocol.protocol_engine.ProtocolStep; - -public class OwnedIdentityDeletionWithContactNotificationProtocol extends ConcreteProtocol { - public OwnedIdentityDeletionWithContactNotificationProtocol(ProtocolManagerSession protocolManagerSession, UID protocolInstanceUid, int currentStateId, Encoded encodedCurrentState, Identity ownedIdentity, PRNGService prng, ObjectMapper jsonObjectMapper) throws Exception { - super(protocolManagerSession, protocolInstanceUid, currentStateId, encodedCurrentState, ownedIdentity, prng, jsonObjectMapper); - } - - - @Override - public int getProtocolId() { - return OWNED_IDENTITY_DELETION_WITH_CONTACT_NOTIFICATION_PROTOCOL_ID; - } - - // region states - - public static final int FINISHED_STATED_ID = 1; - - @Override - public int[] getFinalStateIds() { - return new int[]{FINISHED_STATED_ID}; - } - - - @Override - protected Class getStateClass(int stateId) { - switch (stateId) { - case INITIAL_STATE_ID: - return InitialProtocolState.class; - case FINISHED_STATED_ID: - return FinishedProtocolState.class; - default: - return null; - } - } - - - public static class FinishedProtocolState extends ConcreteProtocolState { - @SuppressWarnings({"unused", "RedundantSuppression"}) - public FinishedProtocolState(Encoded encodedState) throws Exception { - super(FINISHED_STATE_ID); - Encoded[] list = encodedState.decodeList(); - if (list.length != 0) { - throw new Exception(); - } - } - - public FinishedProtocolState() { - super(FINISHED_STATE_ID); - } - - @Override - public Encoded encode() { - return Encoded.of(new Encoded[0]); - } - } - // endregion - - - // region messages - static final int INITIAL_MESSAGE_ID = 0; - static final int CONTACT_OWNED_IDENTITY_WAS_DELETED_MESSAGE_ID = 1; - - - @Override - protected Class getMessageClass(int protocolMessageId) { - switch (protocolMessageId) { - case INITIAL_MESSAGE_ID: - return InitialMessage.class; - case CONTACT_OWNED_IDENTITY_WAS_DELETED_MESSAGE_ID: - return ContactOwnedIdentityWasDeletedMessage.class; - default: - return null; - } - } - - public static class InitialMessage extends ConcreteProtocolMessage { - public InitialMessage(CoreProtocolMessage coreProtocolMessage) { - super(coreProtocolMessage); - } - - @SuppressWarnings({"unused", "RedundantSuppression"}) - public InitialMessage(ReceivedMessage receivedMessage) throws Exception { - super(new CoreProtocolMessage(receivedMessage)); - if (receivedMessage.getInputs().length != 0) { - throw new Exception(); - } - } - - @Override - public int getProtocolMessageId() { - return INITIAL_MESSAGE_ID; - } - - @Override - public Encoded[] getInputs() { - return new Encoded[0]; - } - } - - public static class ContactOwnedIdentityWasDeletedMessage extends ConcreteProtocolMessage { - private final Identity deletedContactOwnedIdentity; - private final byte[] signature; - - public ContactOwnedIdentityWasDeletedMessage(CoreProtocolMessage coreProtocolMessage, Identity deletedContactOwnedIdentity, byte[] signature) { - super(coreProtocolMessage); - this.deletedContactOwnedIdentity = deletedContactOwnedIdentity; - this.signature = signature; - } - - @SuppressWarnings({"unused", "RedundantSuppression"}) - public ContactOwnedIdentityWasDeletedMessage(ReceivedMessage receivedMessage) throws Exception { - super(new CoreProtocolMessage(receivedMessage)); - Encoded[] inputs = receivedMessage.getInputs(); - if (inputs.length != 2) { - throw new Exception(); - } - this.deletedContactOwnedIdentity = inputs[0].decodeIdentity(); - this.signature = inputs[1].decodeBytes(); - } - - @Override - public int getProtocolMessageId() { - return CONTACT_OWNED_IDENTITY_WAS_DELETED_MESSAGE_ID; - } - - @Override - public Encoded[] getInputs() { - return new Encoded[]{ - Encoded.of(deletedContactOwnedIdentity), - Encoded.of(signature), - }; - } - } - - - // endregion - - - - - - - // region steps - @Override - protected Class[] getPossibleStepClasses(int stateId) { - switch (stateId) { - case INITIAL_STATE_ID: - return new Class[]{OwnedIdentityDeletionWithContactNotificationStep.class, ProcessContactOwnedIdentityWasDeletedMessageStep.class}; - case FINISHED_STATED_ID: - default: - return new Class[0]; - } - } - - public static class OwnedIdentityDeletionWithContactNotificationStep extends ProtocolStep { - InitialProtocolState startState; - InitialMessage receivedMessage; - - public OwnedIdentityDeletionWithContactNotificationStep(InitialProtocolState startState, InitialMessage receivedMessage, OwnedIdentityDeletionWithContactNotificationProtocol protocol) throws Exception { - super(ReceptionChannelInfo.createLocalChannelInfo(), receivedMessage, protocol); - this.startState = startState; - this.receivedMessage = receivedMessage; - } - - @Override - public ConcreteProtocolState executeStep() throws Exception { - ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); - - // TODO multi-device: also propagate this information to other owned devices - - //////////// - // disband all owned groups & leave all joined groups - { - Group[] groups = protocolManagerSession.identityDelegate.getGroupsForOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); - for (Group group : groups) { - GroupInformation groupInformation = protocolManagerSession.identityDelegate.getGroupInformation(protocolManagerSession.session, getOwnedIdentity(), group.getGroupOwnerAndUid()); - UID protocolInstanceUid = groupInformation.computeProtocolUid(); - - if (group.getGroupOwner() == null) { - //////////// - // owned group -> kick all members and pending members - if (group.getGroupMembers().length > 0) { - SendChannelInfo[] sendChannelInfos = SendChannelInfo.createAllConfirmedObliviousChannelsInfosForMultipleIdentities(group.getGroupMembers(), getOwnedIdentity()); - for (SendChannelInfo sendChannelInfo : sendChannelInfos) { - try { - CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(sendChannelInfo, - GROUP_MANAGEMENT_PROTOCOL_ID, - protocolInstanceUid, - false); - ChannelMessageToSend messageToSend = new GroupManagementProtocol.KickFromGroupMessage(coreProtocolMessage, groupInformation).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); - } catch (Exception ignored) { - // continue even if there is an exception, contact notification is only best effort! - } - } - } - if (group.getPendingGroupMembers().length > 0) { - Identity[] pendingMemberIdentities = new Identity[group.getPendingGroupMembers().length]; - for (int i = 0; i < pendingMemberIdentities.length; i++) { - pendingMemberIdentities[i] = group.getPendingGroupMembers()[i].identity; - } - - SendChannelInfo[] sendChannelInfos = SendChannelInfo.createAllConfirmedObliviousChannelsInfosForMultipleIdentities(pendingMemberIdentities, getOwnedIdentity()); - for (SendChannelInfo sendChannelInfo : sendChannelInfos) { - try { - CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(sendChannelInfo, - GROUP_MANAGEMENT_PROTOCOL_ID, - protocolInstanceUid, - false); - ChannelMessageToSend messageToSend = new GroupManagementProtocol.KickFromGroupMessage(coreProtocolMessage, groupInformation).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); - } catch (Exception ignored) { - // continue even if there is an exception, contact notification is only best effort! - } - } - } - } else { - //////////// - // joined group -> notify group owner - try { - CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createAllConfirmedObliviousChannelsInfo(group.getGroupOwner(), getOwnedIdentity()), - GROUP_MANAGEMENT_PROTOCOL_ID, - protocolInstanceUid, - false); - ChannelMessageToSend message = new GroupManagementProtocol.NotifyGroupLeftMessage(coreProtocolMessage, groupInformation).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message, getPrng()); - } catch (Exception ignored) { - // continue even if there is an exception, contact notification is only best effort! - } - } - } - } - - //////////// - // leave all groups v2 & disband those where I am the only admin - { - List groupsV2 = protocolManagerSession.identityDelegate.getObvGroupsV2ForOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); - for (ObvGroupV2 groupV2 : groupsV2) { - if (groupV2.groupIdentifier.category == GroupV2.Identifier.CATEGORY_SERVER) { - // only consider non-keycloak groups - try { - // check if I am the only non-pending admin of this group - boolean iAmTheOnlyAdmin; - if (groupV2.ownPermissions.contains(GroupV2.Permission.GROUP_ADMIN)) { - iAmTheOnlyAdmin = true; - for (ObvGroupV2.ObvGroupV2Member member : groupV2.otherGroupMembers) { - if (member.permissions.contains(GroupV2.Permission.GROUP_ADMIN)) { - iAmTheOnlyAdmin = false; - break; - } - } - } else { - iAmTheOnlyAdmin = false; - } - - if (iAmTheOnlyAdmin) { - // delete the blob on the server - GroupV2.BlobKeys blobKeys = protocolManagerSession.identityDelegate.getGroupV2BlobKeys(protocolManagerSession.session, getOwnedIdentity(), groupV2.groupIdentifier); - { - byte[] signature = Signature.sign(Constants.SignatureContext.GROUP_DELETE_ON_SERVER, blobKeys.groupAdminServerAuthenticationPrivateKey.getSignaturePrivateKey(), getPrng()); - CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createServerQueryChannelInfo(getOwnedIdentity(), ServerQuery.Type.createDeleteGroupBlobQuery(groupV2.groupIdentifier, signature)), ConcreteProtocol.GROUPS_V2_PROTOCOL_ID, new UID(getPrng()), false); - ChannelMessageToSend messageToSend = new GroupsV2Protocol.DeleteGroupBlobFromServerMessage(coreProtocolMessage).generateChannelServerQueryMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); - } - - // immediately kick all members - byte[] chainPlaintext = protocolManagerSession.identityDelegate.getGroupV2AdministratorsChain(protocolManagerSession.session, getOwnedIdentity(), groupV2.groupIdentifier).encode().getBytes(); - AuthEncKey encryptionKey = (AuthEncKey) Suite.getKDF(KDF.KDF_SHA256).gen(blobKeys.blobMainSeed, Suite.getDefaultAuthEnc(0).getKDFDelegate())[0]; - EncryptedBytes encryptedChain = Suite.getAuthEnc(encryptionKey).encrypt(encryptionKey, chainPlaintext, getPrng()); - - GroupV2.ServerBlob serverBlob = protocolManagerSession.identityDelegate.getGroupV2ServerBlob(protocolManagerSession.session, getOwnedIdentity(), groupV2.groupIdentifier); - - for (GroupV2.IdentityAndPermissionsAndDetails member : serverBlob.groupMemberIdentityAndPermissionsAndDetailsList) { - if (member.identity.equals(getOwnedIdentity())) { - continue; - } - - byte[] dataToSign = new byte[encryptedChain.length + member.groupInvitationNonce.length]; - System.arraycopy(encryptedChain.getBytes(), 0, dataToSign, 0, encryptedChain.length); - System.arraycopy(member.groupInvitationNonce, 0, dataToSign, encryptedChain.length, member.groupInvitationNonce.length); - - byte[] signature = protocolManagerSession.identityDelegate.signBlock(protocolManagerSession.session, Constants.SignatureContext.GROUP_KICK, dataToSign, getOwnedIdentity(), getPrng()); - - CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createAsymmetricBroadcastChannelInfo(member.identity, getOwnedIdentity()), ConcreteProtocol.GROUPS_V2_PROTOCOL_ID, groupV2.groupIdentifier.computeProtocolInstanceUid(), false); - ChannelMessageToSend messageToSend = new GroupsV2Protocol.KickMessage(coreProtocolMessage, groupV2.groupIdentifier, encryptedChain, signature).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); - } - } else { - byte[] ownGroupInvitationNonce = protocolManagerSession.identityDelegate.getGroupV2OwnGroupInvitationNonce(protocolManagerSession.session, getOwnedIdentity(), groupV2.groupIdentifier); - if (ownGroupInvitationNonce != null) { - // put a group left log on server - // we do not notify the group members: they will refresh the groups when we send them the contact deletion message - byte[] leaveSignature = protocolManagerSession.identityDelegate.signGroupInvitationNonce( - protocolManagerSession.session, - Constants.SignatureContext.GROUP_LEAVE_NONCE, - groupV2.groupIdentifier, - ownGroupInvitationNonce, - null, - getOwnedIdentity(), - getPrng()); - - CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createServerQueryChannelInfo(getOwnedIdentity(), ServerQuery.Type.createPutGroupLogQuery(groupV2.groupIdentifier, leaveSignature)), ConcreteProtocol.GROUPS_V2_PROTOCOL_ID, new UID(getPrng()), false); - ChannelMessageToSend messageToSend = new GroupsV2Protocol.PutGroupLogOnServerMessage(coreProtocolMessage).generateChannelServerQueryMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); - } - } - } catch (Exception ignored) { - // continue even if there is an exception, contact notification is only best effort! - } - } - } - } - - //////////// - // send delete notifications to contacts - { - UID protocolInstanceUid = new UID(getPrng()); - - Identity[] contactIdentities = protocolManagerSession.identityDelegate.getContactsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); - if (contactIdentities.length > 0) { - for (Identity contactIdentity : contactIdentities) { - try { - byte[] signature = protocolManagerSession.identityDelegate.signBlock(protocolManagerSession.session, Constants.SignatureContext.OWNED_IDENTITY_DELETION, contactIdentity.getBytes(), getOwnedIdentity(), getPrng()); - - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAsymmetricBroadcastChannelInfo(contactIdentity, getOwnedIdentity())); - ChannelMessageToSend messageToSend = new ContactOwnedIdentityWasDeletedMessage(coreProtocolMessage, getOwnedIdentity(), signature).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); - } catch (Exception ignored) { - // continue even if there is an exception, contact notification is only best effort! - } - } - - // We no longer send the "legacy" delete contact message as it may mess up the treatment of our new ContactOwnedIdentityWasDeletedMessage - } - } - - - // finally, delete all channels (all notifications message have already been encrypted) and actually delete owned identity - protocolManagerSession.channelDelegate.deleteAllChannelsForOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); - protocolManagerSession.identityDelegate.deleteOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); - - return new FinishedProtocolState(); - } - } - - - public static class ProcessContactOwnedIdentityWasDeletedMessageStep extends ProtocolStep { - InitialProtocolState startState; - ContactOwnedIdentityWasDeletedMessage receivedMessage; - boolean propagated; - - public ProcessContactOwnedIdentityWasDeletedMessageStep(InitialProtocolState startState, ContactOwnedIdentityWasDeletedMessage receivedMessage, OwnedIdentityDeletionWithContactNotificationProtocol protocol) throws Exception { - super((receivedMessage.getReceptionChannelInfo().getChannelType() == ReceptionChannelInfo.ASYMMETRIC_CHANNEL_TYPE) ? ReceptionChannelInfo.createAsymmetricChannelInfo() : ReceptionChannelInfo.createAnyObliviousChannelWithOwnedDeviceInfo(), receivedMessage, protocol); - this.startState = startState; - this.receivedMessage = receivedMessage; - propagated = receivedMessage.getReceptionChannelInfo().getChannelType() != ReceptionChannelInfo.ASYMMETRIC_CHANNEL_TYPE; - } - - - @Override - public ConcreteProtocolState executeStep() throws Exception { - ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); - - { - // check the message is not a replay - if (IdentityDeletionSignatureReceived.exists(protocolManagerSession, getOwnedIdentity(), receivedMessage.signature)) { - Logger.w("Received a ContactOwnedIdentityWasDeletedMessage with a known signature"); - return new FinishedProtocolState(); - } - } - - { - // verify the signature - if (!Signature.verify(Constants.SignatureContext.OWNED_IDENTITY_DELETION, getOwnedIdentity().getBytes(), receivedMessage.deletedContactOwnedIdentity, receivedMessage.signature)) { - Logger.w("Received a ContactOwnedIdentityWasDeletedMessage with an invalid signature"); - return new FinishedProtocolState(); - } - } - - // save the signature to prevent replay - IdentityDeletionSignatureReceived.create(protocolManagerSession, getOwnedIdentity(), receivedMessage.signature); - - if (!propagated) { - // propagate the message to other owned devices - - int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; - if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new ContactOwnedIdentityWasDeletedMessage(coreProtocolMessage, receivedMessage.deletedContactOwnedIdentity, receivedMessage.signature).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); - } - } - - - // now we can delete everything related to this contact - - { - // delete all channels - protocolManagerSession.channelDelegate.deleteObliviousChannelsWithContact(protocolManagerSession.session, getOwnedIdentity(), receivedMessage.deletedContactOwnedIdentity); - } - - { - // deal with group v1 - List groupOwnerAndUids = new ArrayList<>(Arrays.asList(protocolManagerSession.identityDelegate.getGroupOwnerAndUidOfGroupsWhereContactIsPending(protocolManagerSession.session, receivedMessage.deletedContactOwnedIdentity, getOwnedIdentity()))); - groupOwnerAndUids.addAll(Arrays.asList(protocolManagerSession.identityDelegate.getGroupOwnerAndUidsOfGroupsContainingContact(protocolManagerSession.session, receivedMessage.deletedContactOwnedIdentity, getOwnedIdentity()))); - - for (byte[] groupOwnerAndUid : groupOwnerAndUids) { - Group group = protocolManagerSession.identityDelegate.getGroup(protocolManagerSession.session, getOwnedIdentity(), groupOwnerAndUid); - if (!propagated && group.getGroupOwner() == null) { - // I own the group --> properly remove the member from the group and trigger the step to notify others - GroupInformation groupInformation = protocolManagerSession.identityDelegate.getGroupInformation(protocolManagerSession.session, getOwnedIdentity(), groupOwnerAndUid); - - GroupMembersChangedCallback groupMembersChangedCallback = () -> { - CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), ConcreteProtocol.GROUP_MANAGEMENT_PROTOCOL_ID, groupInformation.computeProtocolUid(), false); - ChannelMessageToSend messageToSend = new GroupManagementProtocol.GroupMembersOrDetailsChangedTriggerMessage(coreProtocolMessage, groupInformation).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); - }; - - protocolManagerSession.identityDelegate.removeMembersAndPendingFromGroup(protocolManagerSession.session, groupOwnerAndUid, getOwnedIdentity(), new Identity[]{receivedMessage.deletedContactOwnedIdentity}, groupMembersChangedCallback); - } else { - // I joined the group (or it is propagated) - if (receivedMessage.deletedContactOwnedIdentity.equals(group.getGroupOwner())) { - // the removed contact was the group owner --> leave the group - protocolManagerSession.identityDelegate.leaveGroup(protocolManagerSession.session, groupOwnerAndUid, getOwnedIdentity()); - } else { - // remove the member/pending member before receiving the notification from the group owner - protocolManagerSession.identityDelegate.forcefullyRemoveMemberOrPendingFromJoinedGroup(protocolManagerSession.session, getOwnedIdentity(), groupOwnerAndUid, receivedMessage.deletedContactOwnedIdentity); - } - } - } - } - - - { - // deal with group v2 - for (GroupV2.IdentifierAndAdminStatus identifierAndAdminStatus : protocolManagerSession.identityDelegate.getServerGroupsV2IdentifierAndMyAdminStatusForContact(protocolManagerSession.session, getOwnedIdentity(), receivedMessage.deletedContactOwnedIdentity)) { - if (!propagated && identifierAndAdminStatus.iAmAdmin) { - // I am a group admin --> start the standard group update protocol - ObvGroupV2.ObvGroupV2ChangeSet changeSet = new ObvGroupV2.ObvGroupV2ChangeSet(); - changeSet.removedMembers.add(receivedMessage.deletedContactOwnedIdentity.getBytes()); - - CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), - ConcreteProtocol.GROUPS_V2_PROTOCOL_ID, - identifierAndAdminStatus.groupIdentifier.computeProtocolInstanceUid(), - false); - - ChannelMessageToSend messageToSend = new GroupsV2Protocol.GroupUpdateInitialMessage(coreProtocolMessage, identifierAndAdminStatus.groupIdentifier, changeSet).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); - } - - // always remove contact from the group immediately: if admin, this does not prevent the update to work, if not, we will get an update/disband message soon - protocolManagerSession.identityDelegate.forcefullyRemoveMemberOrPendingFromNonAdminGroupV2(protocolManagerSession.session, getOwnedIdentity(), identifierAndAdminStatus.groupIdentifier, receivedMessage.deletedContactOwnedIdentity); - } - } - - // delete contact, do not fail if there are still some groups (typically, groups v2 where I am admin) - protocolManagerSession.identityDelegate.deleteContactIdentity(protocolManagerSession.session, getOwnedIdentity(), receivedMessage.deletedContactOwnedIdentity, false); - - return new FinishedProtocolState(); - } - } - // endregion -} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/SynchronizationProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/SynchronizationProtocol.java new file mode 100644 index 00000000..78071a35 --- /dev/null +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/SynchronizationProtocol.java @@ -0,0 +1,761 @@ +/* + * Olvid for Android + * Copyright © 2019-2023 Olvid SAS + * + * This file is part of Olvid for Android. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + +package io.olvid.engine.protocol.protocols; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.UUID; + +import io.olvid.engine.crypto.PRNGService; +import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.datatypes.NoAcceptableChannelException; +import io.olvid.engine.datatypes.UID; +import io.olvid.engine.datatypes.containers.ChannelMessageToSend; +import io.olvid.engine.datatypes.containers.DialogType; +import io.olvid.engine.datatypes.containers.ReceptionChannelInfo; +import io.olvid.engine.datatypes.containers.SendChannelInfo; +import io.olvid.engine.encoder.Encoded; +import io.olvid.engine.engine.types.sync.ObvSyncAtom; +import io.olvid.engine.protocol.databases.ReceivedMessage; +import io.olvid.engine.protocol.datatypes.CoreProtocolMessage; +import io.olvid.engine.protocol.datatypes.ProtocolManagerSession; +import io.olvid.engine.protocol.protocol_engine.ConcreteProtocol; +import io.olvid.engine.protocol.protocol_engine.ConcreteProtocolMessage; +import io.olvid.engine.protocol.protocol_engine.ConcreteProtocolState; +import io.olvid.engine.protocol.protocol_engine.InitialProtocolState; +import io.olvid.engine.protocol.protocol_engine.OneWayDialogProtocolMessage; +import io.olvid.engine.protocol.protocol_engine.ProtocolStep; + +public class SynchronizationProtocol extends ConcreteProtocol { + public SynchronizationProtocol(ProtocolManagerSession protocolManagerSession, UID protocolInstanceUid, int currentStateId, Encoded encodedCurrentState, Identity ownedIdentity, PRNGService prng, ObjectMapper jsonObjectMapper) throws Exception { + super(protocolManagerSession, protocolInstanceUid, currentStateId, encodedCurrentState, ownedIdentity, prng, jsonObjectMapper); + } + + @Override + public int getProtocolId() { + return SYNCHRONIZATION_PROTOCOL_ID; + } + +// public static UID computeOngoingProtocolInstanceUid(Identity ownedIdentity, UID currentDeviceUid, UID otherDeviceUid) { +// byte[] bytesOwnedIdentity = ownedIdentity.getBytes(); +// byte[] seed = new byte[bytesOwnedIdentity.length + 2 * UID.UID_LENGTH]; +// System.arraycopy(bytesOwnedIdentity, 0, seed, 0, bytesOwnedIdentity.length); +// if (currentDeviceUid.compareTo(otherDeviceUid) < 0) { +// System.arraycopy(currentDeviceUid.getBytes(), 0, seed, bytesOwnedIdentity.length, UID.UID_LENGTH); +// System.arraycopy(otherDeviceUid.getBytes(), 0, seed, bytesOwnedIdentity.length + UID.UID_LENGTH, UID.UID_LENGTH); +// } else { +// System.arraycopy(otherDeviceUid.getBytes(), 0, seed, bytesOwnedIdentity.length, UID.UID_LENGTH); +// System.arraycopy(currentDeviceUid.getBytes(), 0, seed, bytesOwnedIdentity.length + UID.UID_LENGTH, UID.UID_LENGTH); +// } +// +// Seed prngSeed = new Seed(seed); +// PRNG seededPRNG = Suite.getDefaultPRNG(0, prngSeed); +// return new UID(seededPRNG); +// } + + // region States + +// private static final int ONGOING_SYNC_STATE = 1; + private static final int FINAL_STATE_ID = 99; + + @Override + public int[] getFinalStateIds() { + return new int[]{FINAL_STATE_ID}; + } + + @Override + protected Class getStateClass(int stateId) { + switch (stateId) { + case INITIAL_STATE_ID: + return InitialProtocolState.class; +// case ONGOING_SYNC_STATE: +// return OngoingSyncState.class; + case FINAL_STATE_ID: + return FinalState.class; + default: + return null; + } + } + +// public static class OngoingSyncState extends ConcreteProtocolState { +// private final UID otherDeviceUid; +// private final long currentVersion; +// private final long lastSeenOtherVersion; +// private final HashMap currentSnapshotDict; +// private final HashMap otherSnapshotDict; // can be null +// private final boolean currentlyShowingDiff; +// +// public OngoingSyncState(UID otherDeviceUid, long currentVersion, long lastSeenOtherVersion, HashMap currentSnapshotDict, HashMap otherSnapshotDict, boolean currentlyShowingDiff) { +// super(ONGOING_SYNC_STATE); +// this.otherDeviceUid = otherDeviceUid; +// this.currentVersion = currentVersion; +// this.lastSeenOtherVersion = lastSeenOtherVersion; +// this.currentSnapshotDict = currentSnapshotDict; +// this.otherSnapshotDict = otherSnapshotDict; +// this.currentlyShowingDiff = currentlyShowingDiff; +// } +// +// @SuppressWarnings({"unused", "RedundantSuppression"}) +// public OngoingSyncState(Encoded encodedState) throws Exception { +// super(ONGOING_SYNC_STATE); +// Encoded[] list = encodedState.decodeList(); +// if (list.length == 5) { // the otherSnapshotDict is null +// this.otherDeviceUid = list[0].decodeUid(); +// this.currentVersion = list[1].decodeLong(); +// this.lastSeenOtherVersion = list[2].decodeLong(); +// this.currentSnapshotDict = list[3].decodeDictionary(); +// this.otherSnapshotDict = null; +// this.currentlyShowingDiff = list[4].decodeBoolean(); +// } else if (list.length == 6) { // the otherSnapshotDict is not null +// this.otherDeviceUid = list[0].decodeUid(); +// this.currentVersion = list[1].decodeLong(); +// this.lastSeenOtherVersion = list[2].decodeLong(); +// this.currentSnapshotDict = list[3].decodeDictionary(); +// this.otherSnapshotDict = list[4].decodeDictionary(); +// this.currentlyShowingDiff = list[5].decodeBoolean(); +// } else { +// throw new Exception(); +// } +// } +// +// @Override +// public Encoded encode() { +// if (otherSnapshotDict == null) { +// return Encoded.of(new Encoded[]{ +// Encoded.of(otherDeviceUid), +// Encoded.of(currentVersion), +// Encoded.of(lastSeenOtherVersion), +// Encoded.of(currentSnapshotDict), +// Encoded.of(currentlyShowingDiff), +// }); +// } else { +// return Encoded.of(new Encoded[]{ +// Encoded.of(otherDeviceUid), +// Encoded.of(currentVersion), +// Encoded.of(lastSeenOtherVersion), +// Encoded.of(currentSnapshotDict), +// Encoded.of(otherSnapshotDict), +// Encoded.of(currentlyShowingDiff), +// }); +// } +// } +// } + + + public static class FinalState extends ConcreteProtocolState { + protected FinalState() { + super(FINAL_STATE_ID); + } + + @Override + public Encoded encode() { + return Encoded.of(new Encoded[0]); + } + } + + + // endregion + + + + // region Messages + + public static final int INITIATE_SINGLE_ITEM_SYNC_MESSAGE_ID = 0; + public static final int SINGLE_ITEM_SYNC_MESSAGE_ID = 1; +// public static final int INITIATE_SYNC_MESSAGE_ID = 2; +// public static final int TRIGGER_SYNC_MESSAGE_ID = 3; +// public static final int SNAPSHOT_SYNC_MESSAGE_ID = 4; +// public static final int ATOM_PROCESSED_MESSAGE_ID = 5; + + @Override + protected Class getMessageClass(int protocolMessageId) { + switch (protocolMessageId) { + case INITIATE_SINGLE_ITEM_SYNC_MESSAGE_ID: + return InitiateSingleItemSyncMessage.class; + case SINGLE_ITEM_SYNC_MESSAGE_ID: + return SingleItemSyncMessage.class; +// case INITIATE_SYNC_MESSAGE_ID: +// return InitiateSyncMessage.class; +// case TRIGGER_SYNC_MESSAGE_ID: +// return TriggerSyncMessage.class; +// case SNAPSHOT_SYNC_MESSAGE_ID: +// return SnapshotSyncMessage.class; +// case ATOM_PROCESSED_MESSAGE_ID: +// return AtomProcessedMessage.class; + default: + return null; + } + } + + public static class InitiateSingleItemSyncMessage extends ConcreteProtocolMessage { + final ObvSyncAtom obvSyncAtom; + + public InitiateSingleItemSyncMessage(CoreProtocolMessage coreProtocolMessage, ObvSyncAtom obvSyncAtom) { + super(coreProtocolMessage); + this.obvSyncAtom = obvSyncAtom; + } + + @SuppressWarnings("unused") + public InitiateSingleItemSyncMessage(ReceivedMessage receivedMessage) throws Exception { + super(new CoreProtocolMessage(receivedMessage)); + Encoded[] list = receivedMessage.getInputs(); + if (list.length != 1) { + throw new Exception(); + } + this.obvSyncAtom = ObvSyncAtom.of(list[0]); + } + + @Override + public int getProtocolMessageId() { + return INITIATE_SINGLE_ITEM_SYNC_MESSAGE_ID; + } + + @Override + public Encoded[] getInputs() { + return new Encoded[]{ + obvSyncAtom.encode(), + }; + } + } + + public static class SingleItemSyncMessage extends ConcreteProtocolMessage { + final ObvSyncAtom obvSyncAtom; + + public SingleItemSyncMessage(CoreProtocolMessage coreProtocolMessage, ObvSyncAtom obvSyncAtom) { + super(coreProtocolMessage); + this.obvSyncAtom = obvSyncAtom; + } + + @SuppressWarnings("unused") + public SingleItemSyncMessage(ReceivedMessage receivedMessage) throws Exception { + super(new CoreProtocolMessage(receivedMessage)); + Encoded[] list = receivedMessage.getInputs(); + if (list.length != 1) { + throw new Exception(); + } + this.obvSyncAtom = ObvSyncAtom.of(list[0]); + } + + @Override + public int getProtocolMessageId() { + return SINGLE_ITEM_SYNC_MESSAGE_ID; + } + + @Override + public Encoded[] getInputs() { + return new Encoded[]{ + obvSyncAtom.encode(), + }; + } + } + +// public static class InitiateSyncMessage extends ConcreteProtocolMessage { +// final UID otherDeviceUid; +// +// public InitiateSyncMessage(CoreProtocolMessage coreProtocolMessage, UID otherDeviceUid) { +// super(coreProtocolMessage); +// this.otherDeviceUid = otherDeviceUid; +// } +// +// @SuppressWarnings("unused") +// public InitiateSyncMessage(ReceivedMessage receivedMessage) throws Exception { +// super(new CoreProtocolMessage(receivedMessage)); +// Encoded[] list = receivedMessage.getInputs(); +// if (list.length != 1) { +// throw new Exception(); +// } +// this.otherDeviceUid = list[0].decodeUid(); +// } +// +// @Override +// public int getProtocolMessageId() { +// return INITIATE_SYNC_MESSAGE_ID; +// } +// +// @Override +// public Encoded[] getInputs() { +// return new Encoded[]{ +// Encoded.of(otherDeviceUid), +// }; +// } +// } + +// public static class TriggerSyncMessage extends ConcreteProtocolMessage { +// private final boolean forceSendSnapshot; +// public TriggerSyncMessage(CoreProtocolMessage coreProtocolMessage, boolean forceSendSnapshot) { +// super(coreProtocolMessage); +// this.forceSendSnapshot = forceSendSnapshot; +// } +// +// @SuppressWarnings("unused") +// public TriggerSyncMessage(ReceivedMessage receivedMessage) throws Exception { +// super(new CoreProtocolMessage(receivedMessage)); +// Encoded[] list = receivedMessage.getInputs(); +// if (list.length != 1) { +// throw new Exception(); +// } +// this.forceSendSnapshot = list[0].decodeBoolean(); +// } +// +// @Override +// public int getProtocolMessageId() { +// return TRIGGER_SYNC_MESSAGE_ID; +// } +// +// @Override +// public Encoded[] getInputs() { +// return new Encoded[]{ +// Encoded.of(forceSendSnapshot), +// }; +// } +// } + +// public static class SnapshotSyncMessage extends ConcreteProtocolMessage { +// private final long senderCurrentVersion; +// private final long lastSeenRecipientVersion; +// private final HashMap snapshotDict; +// +// public SnapshotSyncMessage(CoreProtocolMessage coreProtocolMessage, long senderCurrentVersion, long lastSeenRecipientVersion, HashMap snapshotDict) { +// super(coreProtocolMessage); +// this.senderCurrentVersion = senderCurrentVersion; +// this.lastSeenRecipientVersion = lastSeenRecipientVersion; +// this.snapshotDict = snapshotDict; +// } +// +// @SuppressWarnings("unused") +// public SnapshotSyncMessage(ReceivedMessage receivedMessage) throws Exception { +// super(new CoreProtocolMessage(receivedMessage)); +// Encoded[] list = receivedMessage.getInputs(); +// if (list.length != 3) { +// throw new Exception(); +// } +// this.senderCurrentVersion = list[0].decodeLong(); +// this.lastSeenRecipientVersion = list[1].decodeLong(); +// this.snapshotDict = list[2].decodeDictionary(); +// } +// +// @Override +// public int getProtocolMessageId() { +// return SNAPSHOT_SYNC_MESSAGE_ID; +// } +// +// @Override +// public Encoded[] getInputs() { +// return new Encoded[]{ +// Encoded.of(senderCurrentVersion), +// Encoded.of(lastSeenRecipientVersion), +// Encoded.of(snapshotDict), +// }; +// } +// } + +// public static class AtomProcessedMessage extends EmptyProtocolMessage { +// public AtomProcessedMessage(CoreProtocolMessage coreProtocolMessage) { +// super(coreProtocolMessage); +// } +// +// @SuppressWarnings("unused") +// public AtomProcessedMessage(ReceivedMessage receivedMessage) throws Exception { +// super(receivedMessage); +// } +// +// @Override +// public int getProtocolMessageId() { +// return ATOM_PROCESSED_MESSAGE_ID; +// } +// } + + + // endregion + + + + + + // region Steps + + @Override + protected Class[] getPossibleStepClasses(int stateId) { + switch (stateId) { + case INITIAL_STATE_ID: + return new Class[]{SendSingleItemSyncMessageStep.class, ProcessSingleItemSyncMessageStep.class/*, UpdateStateAndSendSyncMessageStep.class*/}; +// case ONGOING_SYNC_STATE: +// return new Class[]{UpdateStateAndSendSyncMessageStep.class}; + case FINAL_STATE_ID: + default: + return new Class[0]; + } + } + + public static class SendSingleItemSyncMessageStep extends ProtocolStep { + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private final InitialProtocolState startState; + private final InitiateSingleItemSyncMessage receivedMessage; + + public SendSingleItemSyncMessageStep(InitialProtocolState startState, InitiateSingleItemSyncMessage receivedMessage, SynchronizationProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createLocalChannelInfo(), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + // simply send the ObvSyncItem to all other devices + UID[] otherDeviceUids = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); + if (otherDeviceUids.length > 0) { + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new SingleItemSyncMessage(coreProtocolMessage, receivedMessage.obvSyncAtom).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } + +// // send an AtomProcessedMessage to all ongoing instances of the synchronisation protocol +// UID currentDeviceUid = protocolManagerSession.identityDelegate.getCurrentDeviceUidOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); +// for (UID otherDeviceUid : otherDeviceUids) { +// CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), +// ConcreteProtocol.SYNCHRONIZATION_PROTOCOL_ID, +// computeOngoingProtocolInstanceUid(getOwnedIdentity(), currentDeviceUid, otherDeviceUid), +// false); +// ChannelMessageToSend message = new AtomProcessedMessage(coreProtocolMessage).generateChannelProtocolMessageToSend(); +// protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message, getPrng()); +// } + } + + + return new FinalState(); + } + } + + public static class ProcessSingleItemSyncMessageStep extends ProtocolStep { + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private final InitialProtocolState startState; + private final SingleItemSyncMessage receivedMessage; + + public ProcessSingleItemSyncMessageStep(InitialProtocolState startState, SingleItemSyncMessage receivedMessage, SynchronizationProtocol protocol) throws Exception { + super(ReceptionChannelInfo.createAnyObliviousChannelWithOwnedDeviceInfo(), receivedMessage, protocol); + this.startState = startState; + this.receivedMessage = receivedMessage; + } + + @Override + public ConcreteProtocolState executeStep() throws Exception { + ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); + + // check whether this item should be processed at app level or identity manager level + if (receivedMessage.obvSyncAtom.isAppSyncItem()) { + // create a one way app dialog to send the sync item to the app + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createUserInterfaceChannelInfo(getOwnedIdentity(), DialogType.createSyncItemToApplyDialog(receivedMessage.obvSyncAtom), UUID.randomUUID())); + ChannelMessageToSend messageToSend = new OneWayDialogProtocolMessage(coreProtocolMessage).generateChannelDialogMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } else { + // request the identity manager to process the sync item + protocolManagerSession.identityDelegate.processSyncItem(protocolManagerSession.session, getOwnedIdentity(), receivedMessage.obvSyncAtom); + } + +// { +// // for all instances of the synchronisation protocol, send an AtomProcessedMessage to refresh the protocol's currentSnapshot +// UID[] otherDeviceUids = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); +// if (otherDeviceUids.length > 0) { // this should always be the case! +// UID currentDeviceUid = protocolManagerSession.identityDelegate.getCurrentDeviceUidOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); +// for (UID otherDeviceUid : otherDeviceUids) { +// CoreProtocolMessage coreProtocolMessage = new CoreProtocolMessage(SendChannelInfo.createLocalChannelInfo(getOwnedIdentity()), +// ConcreteProtocol.SYNCHRONIZATION_PROTOCOL_ID, +// computeOngoingProtocolInstanceUid(getOwnedIdentity(), currentDeviceUid, otherDeviceUid), +// false); +// ChannelMessageToSend message = new AtomProcessedMessage(coreProtocolMessage).generateChannelProtocolMessageToSend(); +// protocolManagerSession.channelDelegate.post(protocolManagerSession.session, message, getPrng()); +// } +// } +// } + + return new FinalState(); + } + } + + +// public static class UpdateStateAndSendSyncMessageStep extends ProtocolStep { +// private final ConcreteProtocolState startState; +// private final boolean ignoreMessage; +// private boolean sendOurSnapshot; +// private final UID otherDeviceUid; +// private final long currentVersion; +// private final long lastSeenOtherVersion; +// private final HashMap currentSnapshotDict; +// private final HashMap otherSnapshotDict; +// private final boolean currentlyShowingDiff; +// private final long receivedCurrentVersion; +// private final long receivedLastSeenOtherVersion; +// private final HashMap receivedSnapshotDict; +// +// @SuppressWarnings("unused") +// public UpdateStateAndSendSyncMessageStep(InitialProtocolState startState, InitiateSyncMessage receivedMessage, SynchronizationProtocol protocol) throws Exception { +// super(ReceptionChannelInfo.createLocalChannelInfo(), receivedMessage, protocol); +// this.startState = null; +// this.ignoreMessage = false; +// this.sendOurSnapshot = false; +// this.otherDeviceUid = receivedMessage.otherDeviceUid; +// this.currentVersion = -1; +// this.lastSeenOtherVersion = -1; +// this.currentSnapshotDict = null; +// this.otherSnapshotDict = null; +// this.currentlyShowingDiff = false; +// this.receivedCurrentVersion = -1; +// this.receivedLastSeenOtherVersion = -1; +// this.receivedSnapshotDict = null; +// } +// +// @SuppressWarnings("unused") +// public UpdateStateAndSendSyncMessageStep(InitialProtocolState startState, TriggerSyncMessage receivedMessage, SynchronizationProtocol protocol) throws Exception { +// super(ReceptionChannelInfo.createLocalChannelInfo(), receivedMessage, protocol); +// this.startState = null; +// this.ignoreMessage = true; +// this.sendOurSnapshot = false; +// this.otherDeviceUid = null; +// this.currentVersion = -1; +// this.lastSeenOtherVersion = -1; +// this.currentSnapshotDict = null; +// this.otherSnapshotDict = null; +// this.currentlyShowingDiff = false; +// this.receivedCurrentVersion = -1; +// this.receivedLastSeenOtherVersion = -1; +// this.receivedSnapshotDict = null; +// } +// +// @SuppressWarnings("unused") +// public UpdateStateAndSendSyncMessageStep(InitialProtocolState startState, SnapshotSyncMessage receivedMessage, SynchronizationProtocol protocol) throws Exception { +// super(ReceptionChannelInfo.createAnyObliviousChannelWithOwnedDeviceInfo(), receivedMessage, protocol); +// this.startState = null; +// this.ignoreMessage = false; +// this.sendOurSnapshot = false; +// this.otherDeviceUid = receivedMessage.getReceptionChannelInfo().getRemoteDeviceUid(); +// this.currentVersion = -1; +// this.lastSeenOtherVersion = -1; +// this.currentSnapshotDict = null; +// this.otherSnapshotDict = null; +// this.currentlyShowingDiff = false; +// this.receivedCurrentVersion = receivedMessage.senderCurrentVersion; +// this.receivedLastSeenOtherVersion = receivedMessage.lastSeenRecipientVersion; +// this.receivedSnapshotDict = receivedMessage.snapshotDict; +// } +// +// @SuppressWarnings("unused") +// public UpdateStateAndSendSyncMessageStep(InitialProtocolState startState, AtomProcessedMessage receivedMessage, SynchronizationProtocol protocol) throws Exception { +// super(ReceptionChannelInfo.createLocalChannelInfo(), receivedMessage, protocol); +// this.startState = null; +// this.ignoreMessage = true; +// this.sendOurSnapshot = false; +// this.otherDeviceUid = null; +// this.currentVersion = -1; +// this.lastSeenOtherVersion = -1; +// this.currentSnapshotDict = null; +// this.otherSnapshotDict = null; +// this.currentlyShowingDiff = false; +// this.receivedCurrentVersion = -1; +// this.receivedLastSeenOtherVersion = -1; +// this.receivedSnapshotDict = null; +// } +// +// @SuppressWarnings("unused") +// public UpdateStateAndSendSyncMessageStep(OngoingSyncState startState, TriggerSyncMessage receivedMessage, SynchronizationProtocol protocol) throws Exception { +// super(ReceptionChannelInfo.createLocalChannelInfo(), receivedMessage, protocol); +// this.startState = startState; +// this.ignoreMessage = false; +// this.sendOurSnapshot = receivedMessage.forceSendSnapshot; +// this.otherDeviceUid = startState.otherDeviceUid; +// this.currentVersion = startState.currentVersion; +// this.lastSeenOtherVersion = startState.lastSeenOtherVersion; +// this.currentSnapshotDict = startState.currentSnapshotDict; +// this.otherSnapshotDict = startState.otherSnapshotDict; +// this.currentlyShowingDiff = startState.currentlyShowingDiff; +// this.receivedCurrentVersion = -1; +// this.receivedLastSeenOtherVersion = -1; +// this.receivedSnapshotDict = null; +// } +// +// @SuppressWarnings("unused") +// public UpdateStateAndSendSyncMessageStep(OngoingSyncState startState, InitiateSyncMessage receivedMessage, SynchronizationProtocol protocol) throws Exception { +// super(ReceptionChannelInfo.createLocalChannelInfo(), receivedMessage, protocol); +// this.startState = startState; +// this.ignoreMessage = true; +// this.sendOurSnapshot = false; +// this.otherDeviceUid = null; +// this.currentVersion = -1; +// this.lastSeenOtherVersion = -1; +// this.currentSnapshotDict = null; +// this.otherSnapshotDict = null; +// this.currentlyShowingDiff = false; +// this.receivedCurrentVersion = -1; +// this.receivedLastSeenOtherVersion = -1; +// this.receivedSnapshotDict = null; +// } +// +// @SuppressWarnings("unused") +// public UpdateStateAndSendSyncMessageStep(OngoingSyncState startState, SnapshotSyncMessage receivedMessage, SynchronizationProtocol protocol) throws Exception { +// super(ReceptionChannelInfo.createAnyObliviousChannelWithOwnedDeviceInfo(), receivedMessage, protocol); +// this.startState = startState; +// this.ignoreMessage = !Objects.equals(receivedMessage.getReceptionChannelInfo().getRemoteDeviceUid(), startState.otherDeviceUid); +// this.sendOurSnapshot = false; +// this.otherDeviceUid = startState.otherDeviceUid; +// this.currentVersion = startState.currentVersion; +// this.lastSeenOtherVersion = startState.lastSeenOtherVersion; +// this.currentSnapshotDict = startState.currentSnapshotDict; +// this.otherSnapshotDict = startState.otherSnapshotDict; +// this.currentlyShowingDiff = startState.currentlyShowingDiff; +// this.receivedCurrentVersion = receivedMessage.senderCurrentVersion; +// this.receivedLastSeenOtherVersion = receivedMessage.lastSeenRecipientVersion; +// this.receivedSnapshotDict = receivedMessage.snapshotDict; +// } +// +// @SuppressWarnings("unused") +// public UpdateStateAndSendSyncMessageStep(OngoingSyncState startState, AtomProcessedMessage receivedMessage, SynchronizationProtocol protocol) throws Exception { +// super(ReceptionChannelInfo.createLocalChannelInfo(), receivedMessage, protocol); +// this.startState = startState; +// this.ignoreMessage = !startState.currentlyShowingDiff; // simply ignore atom processed messages if not currently showing a diff +// this.sendOurSnapshot = false; +// this.otherDeviceUid = startState.otherDeviceUid; +// this.currentVersion = startState.currentVersion; +// this.lastSeenOtherVersion = startState.lastSeenOtherVersion; +// this.currentSnapshotDict = startState.currentSnapshotDict; +// this.otherSnapshotDict = startState.otherSnapshotDict; +// this.currentlyShowingDiff = startState.currentlyShowingDiff; +// this.receivedCurrentVersion = -1; +// this.receivedLastSeenOtherVersion = -1; +// this.receivedSnapshotDict = null; +// } +// +// +// @Override +// public ConcreteProtocolState executeStep() throws Exception { +// ProtocolManagerSession protocolManagerSession = getProtocolManagerSession(); +// +// // check if message should be ignored +// if (ignoreMessage) { +// return startState == null ? new FinalState() : startState; +// } +// +// // check the protocolUid matches what we expect +// UID currentDeviceUid = protocolManagerSession.identityDelegate.getCurrentDeviceUidOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()); +// if (!Objects.equals(computeOngoingProtocolInstanceUid(getOwnedIdentity(), currentDeviceUid, otherDeviceUid), getProtocolInstanceUid())) { +// // this can only happen if the startState is the InitialProtocolState, otherwise the protocolInstanceUid has already been checked +// Logger.w("In SynchronizationProtocol.UpdateStateAndSendSyncMessageStep, bad protocolInstanceUid!"); +// return new FinalState(); +// } +// +// // check the otherDeviceUid has a channel --> if not, finish the protocol. A new one will be started whenever a channel is created +// if (!protocolManagerSession.channelDelegate.checkIfObliviousChannelIsConfirmed(protocolManagerSession.session, getOwnedIdentity(), otherDeviceUid, getOwnedIdentity())) { +// return new FinalState(); +// } +// +// +// ObvSyncSnapshot currentSnapshot = (currentSnapshotDict == null) ? null : ObvSyncSnapshot.fromEncodedDictionary(currentSnapshotDict, protocolManagerSession.identityBackupAndSyncDelegate, protocolManagerSession.appBackupAndSyncDelegate); +// ObvSyncSnapshot otherSnapshot = (otherSnapshotDict == null) ? null : ObvSyncSnapshot.fromEncodedDictionary(otherSnapshotDict, protocolManagerSession.identityBackupAndSyncDelegate, protocolManagerSession.appBackupAndSyncDelegate); +// ObvSyncSnapshot receivedSnapshot = (receivedSnapshotDict == null) ? null : ObvSyncSnapshot.fromEncodedDictionary(receivedSnapshotDict, protocolManagerSession.identityBackupAndSyncDelegate, protocolManagerSession.appBackupAndSyncDelegate); +// +// long updatedCurrentVersion; +// long updatedLastSeenOtherVersion; +// ObvSyncSnapshot updatedCurrentSnapshot; +// ObvSyncSnapshot updatedOtherSnapshot; +// +// // if we received a snapshot, check if it is outdated +// if (receivedCurrentVersion != -1 && receivedSnapshot != null) { +// if (lastSeenOtherVersion > receivedCurrentVersion) { +// // we received an old snapshot, ignore it +// return startState == null ? new FinalState() : startState; +// } else if (lastSeenOtherVersion == receivedCurrentVersion) { +// // numbers are equal: weird! Check if the snapshot has changed +// if (otherSnapshot != null && otherSnapshot.areContentsTheSame(receivedSnapshot)) { +// return startState; +// } +// } +// updatedLastSeenOtherVersion = receivedCurrentVersion; +// updatedOtherSnapshot = receivedSnapshot; +// } else { +// // no change +// updatedLastSeenOtherVersion = lastSeenOtherVersion; +// updatedOtherSnapshot = otherSnapshot; +// } +// +// // check the other device has a coherent version of our snapshot +// if (receivedLastSeenOtherVersion != -1 && receivedLastSeenOtherVersion > currentVersion) { +// // the other device has a larger version than our current --> we probably restarted the protocol and not them +// // 1. update our currentVersion to be larger +// updatedCurrentVersion = receivedLastSeenOtherVersion + 1; +// // 2. we need to send a snapshot in all cases +// sendOurSnapshot = true; +// } else { +// updatedCurrentVersion = currentVersion; +// } +// +// // get our current snapshot and see if anything changed +// updatedCurrentSnapshot = ObvSyncSnapshot.get(getOwnedIdentity(), protocolManagerSession.identityBackupAndSyncDelegate, protocolManagerSession.appBackupAndSyncDelegate); +// boolean currentSnapshotChanged = !updatedCurrentSnapshot.areContentsTheSame(currentSnapshot); +// +// // if something changed, increment version and send the new snapshot to other device +// if (currentSnapshotChanged) { +// updatedCurrentVersion++; +// sendOurSnapshot = true; +// } +// +// ////// +// // decide whether we should compute a diff to show to the user. This will be the case if: +// // - we are currently showing a diff +// // - OR we are not showing a diff, but we received a snapshot with a receivedLastSeenOtherVersion == currentVersion +// // In both cases, if the diff we compute is empty, we stop showing a diff to the user +// +// boolean shouldComputeDiff = updatedOtherSnapshot != null && (this.currentlyShowingDiff || (receivedLastSeenOtherVersion == currentVersion)); +// +// List computedDiffs; +// if (shouldComputeDiff) { +// // we compute a diff between our updatedSnapshot and the updatedOtherSnapshot +// computedDiffs = updatedCurrentSnapshot.computeDiff(updatedOtherSnapshot); +// } else { +// computedDiffs = Collections.emptyList(); +// } +// +// HashMap updatedCurrentSnapshotDict = updatedCurrentSnapshot.toEncodedDictionary(protocolManagerSession.identityBackupAndSyncDelegate, protocolManagerSession.appBackupAndSyncDelegate); +// +// if (sendOurSnapshot) { +// CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createObliviousChannelInfo(getOwnedIdentity(), getOwnedIdentity(), new UID[]{this.otherDeviceUid}, true)); +// ChannelMessageToSend messageToSend = new SnapshotSyncMessage(coreProtocolMessage, updatedCurrentVersion, updatedLastSeenOtherVersion, updatedCurrentSnapshotDict).generateChannelProtocolMessageToSend(); +// protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); +// } +// +// boolean shouldShowDiff = !computedDiffs.isEmpty(); +// if (shouldShowDiff) { +// // TODO: send the diff to the app +// } else if (currentlyShowingDiff) { +// // TODO: send an empty diff to the app +// } +// +// return new OngoingSyncState( +// otherDeviceUid, +// updatedCurrentVersion, +// updatedLastSeenOtherVersion, +// updatedCurrentSnapshotDict, +// updatedOtherSnapshot == null ? null : updatedOtherSnapshot.toEncodedDictionary(protocolManagerSession.identityBackupAndSyncDelegate, protocolManagerSession.appBackupAndSyncDelegate), +// shouldShowDiff +// ); +// } +// } + + // endregion +} diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/TrustEstablishmentWithMutualScanProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/TrustEstablishmentWithMutualScanProtocol.java index dd3c6b6e..924a31a3 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/TrustEstablishmentWithMutualScanProtocol.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/TrustEstablishmentWithMutualScanProtocol.java @@ -27,6 +27,7 @@ import io.olvid.engine.crypto.Signature; import io.olvid.engine.datatypes.Constants; import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.datatypes.NoAcceptableChannelException; import io.olvid.engine.datatypes.UID; import io.olvid.engine.datatypes.containers.ChannelMessageToSend; import io.olvid.engine.datatypes.containers.ReceptionChannelInfo; @@ -408,9 +409,11 @@ public ConcreteProtocolState executeStep() throws Exception { // send propagate messages int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new AlicePropagatesQrCodeMessage(coreProtocolMessage, receivedMessage.contactIdentity, receivedMessage.signature).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new AlicePropagatesQrCodeMessage(coreProtocolMessage, receivedMessage.contactIdentity, receivedMessage.signature).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } @@ -488,11 +491,11 @@ public ConcreteProtocolState executeStep() throws Exception { // signature is valid and fresh --> create the contact (if it does not already exists) if (!protocolManagerSession.identityDelegate.isIdentityAContactOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity(), receivedMessage.aliceIdentity)) { protocolManagerSession.identityDelegate.addContactIdentity(protocolManagerSession.session, receivedMessage.aliceIdentity, receivedMessage.serializedAliceDetails, getOwnedIdentity(), TrustOrigin.createDirectTrustOrigin(System.currentTimeMillis()), true); - } else { + } else { protocolManagerSession.identityDelegate.addTrustOriginToContact(protocolManagerSession.session, receivedMessage.aliceIdentity, getOwnedIdentity(), TrustOrigin.createDirectTrustOrigin(System.currentTimeMillis()), true); } - for (UID contactDeviceUid: receivedMessage.aliceDeviceUids) { - protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), receivedMessage.aliceIdentity, contactDeviceUid); + for (UID contactDeviceUid : receivedMessage.aliceDeviceUids) { + protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), receivedMessage.aliceIdentity, contactDeviceUid, false); } } @@ -511,9 +514,11 @@ public ConcreteProtocolState executeStep() throws Exception { // propagate the message to other devices int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new BobPropagatesSignatureMessage(coreProtocolMessage, receivedMessage.aliceIdentity, receivedMessage.signature, receivedMessage.serializedAliceDetails, receivedMessage.aliceDeviceUids).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new BobPropagatesSignatureMessage(coreProtocolMessage, receivedMessage.aliceIdentity, receivedMessage.signature, receivedMessage.serializedAliceDetails, receivedMessage.aliceDeviceUids).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } @@ -577,7 +582,7 @@ public ConcreteProtocolState executeStep() throws Exception { protocolManagerSession.identityDelegate.addTrustOriginToContact(protocolManagerSession.session, receivedMessage.aliceIdentity, getOwnedIdentity(), TrustOrigin.createDirectTrustOrigin(System.currentTimeMillis()), true); } for (UID contactDeviceUid : receivedMessage.aliceDeviceUids) { - protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), receivedMessage.aliceIdentity, contactDeviceUid); + protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), receivedMessage.aliceIdentity, contactDeviceUid, false); } } @@ -618,7 +623,7 @@ public ConcreteProtocolState executeStep() throws Exception { protocolManagerSession.identityDelegate.addTrustOriginToContact(protocolManagerSession.session, startState.bobIdentity, getOwnedIdentity(), TrustOrigin.createDirectTrustOrigin(System.currentTimeMillis()), true); } for (UID contactDeviceUid : receivedMessage.bobDeviceUids) { - protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), startState.bobIdentity, contactDeviceUid); + protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), startState.bobIdentity, contactDeviceUid, false); } } diff --git a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/TrustEstablishmentWithSasProtocol.java b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/TrustEstablishmentWithSasProtocol.java index 1733e789..22f72f8a 100644 --- a/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/TrustEstablishmentWithSasProtocol.java +++ b/obv_engine/engine/src/main/java/io/olvid/engine/protocol/protocols/TrustEstablishmentWithSasProtocol.java @@ -32,6 +32,7 @@ import io.olvid.engine.crypto.Suite; import io.olvid.engine.datatypes.Constants; import io.olvid.engine.datatypes.Identity; +import io.olvid.engine.datatypes.NoAcceptableChannelException; import io.olvid.engine.datatypes.Seed; import io.olvid.engine.datatypes.UID; import io.olvid.engine.datatypes.containers.ChannelMessageToSend; @@ -68,7 +69,6 @@ public int getProtocolId() { // region States - // Alice's side static final int WAITING_FOR_SEED_STATE_ID = 1; // Bob's side @@ -905,9 +905,11 @@ public ConcreteProtocolState executeStep() throws Exception { // Propagate invitation to other owned devices int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagateInvitationToAliceDevicesMessage(coreProtocolMessage, receivedMessage.contactIdentity, receivedMessage.contactDisplayName, commitmentOutput.decommitment, seedAliceForSas).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateInvitationToAliceDevicesMessage(coreProtocolMessage, receivedMessage.contactIdentity, receivedMessage.contactDisplayName, commitmentOutput.decommitment, seedAliceForSas).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } @@ -996,9 +998,11 @@ public ConcreteProtocolState executeStep() throws Exception { // Propagate invitation to other owned devices int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagateCommitmentToBobDevicesMessage(coreProtocolMessage, receivedMessage.contactIdentity, receivedMessage.contactSerializedDetails, receivedMessage.contactDeviceUids, receivedMessage.commitment).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateCommitmentToBobDevicesMessage(coreProtocolMessage, receivedMessage.contactIdentity, receivedMessage.contactSerializedDetails, receivedMessage.contactDeviceUids, receivedMessage.commitment).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } @@ -1069,9 +1073,11 @@ public ConcreteProtocolState executeStep() throws Exception { // Propagate the accept/reject to other owned devices int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagateConfirmationToBobDevicesMessage(coreProtocolMessage, receivedMessage.invitationAccepted).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateConfirmationToBobDevicesMessage(coreProtocolMessage, receivedMessage.invitationAccepted).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } @@ -1290,9 +1296,11 @@ public ConcreteProtocolState executeStep() throws Exception { // propagate the entered sas to other owned devices int numberOfOtherDevices = protocolManagerSession.identityDelegate.getOtherDeviceUidsOfOwnedIdentity(protocolManagerSession.session, getOwnedIdentity()).length; if (numberOfOtherDevices > 0) { - CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); - ChannelMessageToSend messageToSend = new PropagateEnteredSasToOtherDevicesMessage(coreProtocolMessage, receivedMessage.sasEnteredByUser).generateChannelProtocolMessageToSend(); - protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + try { + CoreProtocolMessage coreProtocolMessage = buildCoreProtocolMessage(SendChannelInfo.createAllOwnedConfirmedObliviousChannelsInfo(getOwnedIdentity())); + ChannelMessageToSend messageToSend = new PropagateEnteredSasToOtherDevicesMessage(coreProtocolMessage, receivedMessage.sasEnteredByUser).generateChannelProtocolMessageToSend(); + protocolManagerSession.channelDelegate.post(protocolManagerSession.session, messageToSend, getPrng()); + } catch (NoAcceptableChannelException ignored) { } } } @@ -1415,7 +1423,7 @@ public ConcreteProtocolState executeStep() throws Exception { protocolManagerSession.identityDelegate.addTrustOriginToContact(protocolManagerSession.session, startState.contactIdentity, getOwnedIdentity(), TrustOrigin.createDirectTrustOrigin(System.currentTimeMillis()), true); } for (UID contactDeviceUid: startState.contactDeviceUids) { - protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), startState.contactIdentity, contactDeviceUid); + protocolManagerSession.identityDelegate.addDeviceForContactIdentity(protocolManagerSession.session, getOwnedIdentity(), startState.contactIdentity, contactDeviceUid, false); } diff --git a/obv_messenger/.idea/inspectionProfiles/Project_Default.xml b/obv_messenger/.idea/inspectionProfiles/Project_Default.xml index fa85284c..a2c70b1a 100644 --- a/obv_messenger/.idea/inspectionProfiles/Project_Default.xml +++ b/obv_messenger/.idea/inspectionProfiles/Project_Default.xml @@ -1,6 +1,9 @@