From b032eb9c9b4b98a1a256d3d03863866bb4136ec8 Mon Sep 17 00:00:00 2001 From: Mo Date: Fri, 30 Jun 2023 09:01:56 -0500 Subject: [PATCH] internal: incomplete vault systems behind feature flag (#2340) --- ...ommon-npm-1.48.3-83aea658e0-191d97879d.zip | Bin 0 -> 36111 bytes ...lugin-npm-5.59.9-3d09cd2e8f-bd2428e307.zip | Bin 0 -> 753085 bytes ...arser-npm-5.59.9-3841845448-69b07d0a5b.zip | Bin 0 -> 10224 bytes ...nager-npm-5.59.9-c9c714cb56-362c22662d.zip | Bin 0 -> 324961 bytes ...utils-npm-5.59.9-fc3a85cbad-6bc2619c50.zip | Bin 0 -> 51699 bytes ...types-npm-5.59.9-9719f93248-283f8fee1e.zip | Bin 0 -> 43862 bytes ...stree-npm-5.59.9-ec2ce6608c-c0c9b81f20.zip | Bin 0 -> 183638 bytes ...utils-npm-5.59.9-d1ab6a9f9a-22ec596288.zip | Bin 0 -> 178744 bytes ...-keys-npm-5.59.9-3e52021052-2909ce761f.zip | Bin 0 -> 11298 bytes package.json | 1 - packages/api/package.json | 2 +- .../Subscription/SubscriptionApiService.ts | 4 +- .../Domain/Client/User/UserApiOperations.ts | 1 + .../src/Domain/Client/User/UserApiService.ts | 23 +- .../Client/User/UserApiServiceInterface.ts | 4 + packages/api/src/Domain/Http/HttpService.ts | 21 + .../src/Domain/Http/HttpServiceInterface.ts | 2 + .../src/Domain/Request/ApiEndpointParam.ts | 7 - .../CreateAsymmetricMessageParams.ts | 5 + .../DeleteAsymmetricMessageRequestParams.ts | 3 + ...OutboundAsymmetricMessagesRequestParams.ts | 2 + .../GetUserAsymmetricMessagesRequestParams.ts | 2 + .../UpdateAsymmetricMessageParams.ts | 4 + .../CreateSharedVaultValetTokenParams.ts | 12 + .../SharedVault/SharedVaultMoveType.ts | 1 + .../AcceptInviteRequestParams.ts | 4 + .../CreateSharedVaultInviteParams.ts | 8 + .../DeclineInviteRequestParams.ts | 4 + ...eleteAllSharedVaultInvitesRequestParams.ts | 3 + .../DeleteInviteRequestParams.ts | 4 + .../GetOutboundUserInvitesRequestParams.ts | 2 + .../GetSharedVaultInvitesRequestParams.ts | 3 + .../GetUserInvitesRequestParams.ts | 2 + .../UpdateSharedVaultInviteParams.ts | 8 + .../DeleteSharedVaultUserRequestParams.ts | 4 + .../GetSharedVaultUsersRequestParams.ts | 3 + .../SubscriptionInviteCancelRequestParams.ts | 2 +- .../SubscriptionInviteDeclineRequestParams.ts | 2 +- .../SubscriptionInviteListRequestParams.ts | 2 +- .../SubscriptionInviteRequestParams.ts | 2 +- .../User/UserRegistrationRequestParams.ts | 6 +- .../Request/User/UserUpdateRequestParams.ts | 7 + packages/api/src/Domain/Request/index.ts | 2 +- .../CreateAsymmetricMessageResponse.ts | 5 + .../DeleteAsymmetricMessageResponse.ts | 3 + .../GetOutboundAsymmetricMessagesResponse.ts | 5 + .../GetUserAsymmetricMessagesResponse.ts | 5 + .../UpdateAsymmetricMessageResponse.ts | 5 + .../Revision/GetRevisionResponseBody.ts | 3 + .../SharedVault/CreateSharedVaultResponse.ts | 6 + .../CreateSharedVaultValetTokenResponse.ts | 3 + .../SharedVault/GetSharedVaultsResponse.ts | 5 + .../AcceptInviteResponse.ts | 3 + .../CreateSharedVaultInviteResponse.ts | 5 + .../DeclineInviteResponse.ts | 3 + .../DeleteAllSharedVaultInvitesResponse.ts | 3 + .../DeleteInviteResponse.ts | 3 + .../GetOutboundUserInvitesResponse.ts | 5 + .../GetSharedVaultInvitesResponse.ts | 5 + .../GetUserInvitesResponse.ts | 5 + .../UpdateSharedVaultInviteResponse.ts | 5 + .../DeleteSharedVaultUserResponse.ts | 3 + .../GetSharedVaultUsersResponse.ts | 5 + .../Response/User/UserUpdateResponse.ts | 6 + packages/api/src/Domain/Response/index.ts | 2 + .../AsymmetricMessageServer.ts | 41 + .../AsymmetricMessageServerInterface.ts | 17 + .../Domain/Server/AsymmetricMessage/Paths.ts | 9 + .../src/Domain/Server/SharedVault/Paths.ts | 7 + .../Server/SharedVault/SharedVaultServer.ts | 37 + .../SharedVault/SharedVaultServerInterface.ts | 17 + .../Domain/Server/SharedVaultInvites/Paths.ts | 16 + .../SharedVaultInvitesServer.ts | 75 ++ .../SharedVaultInvitesServerInterface.ts | 35 + .../Domain/Server/SharedVaultUsers/Paths.ts | 5 + .../SharedVaultUsersServer.ts | 22 + .../SharedVaultUsersServerInterface.ts | 13 + packages/api/src/Domain/Server/User/Paths.ts | 1 + .../api/src/Domain/Server/User/UserServer.ts | 6 + .../Domain/Server/User/UserServerInterface.ts | 3 + packages/api/src/Domain/Server/index.ts | 18 + packages/encryption/package.json | 17 +- packages/encryption/src/Domain/Algorithm.ts | 18 +- .../src/Domain/Keys/ItemsKey/ItemsKey.ts | 5 +- .../KeySystemItemsKey/KeySystemItemsKey.ts | 41 + .../KeySystemItemsKeyMutator.ts | 3 + .../Keys/KeySystemItemsKey/Registration.ts | 10 + .../src/Domain/Keys/RootKey/Functions.ts | 17 +- .../src/Domain/Keys/RootKey/RootKey.ts | 12 +- .../src/Domain/Operator/001/Operator001.ts | 92 +- .../src/Domain/Operator/002/Operator002.ts | 31 +- .../src/Domain/Operator/003/Operator003.ts | 18 +- .../src/Domain/Operator/004/MockedCrypto.ts | 87 ++ .../Domain/Operator/004/Operator004.spec.ts | 58 +- .../src/Domain/Operator/004/Operator004.ts | 396 +++----- .../Asymmetric/AsymmetricDecrypt.spec.ts | 81 ++ .../UseCase/Asymmetric/AsymmetricDecrypt.ts | 48 + .../Asymmetric/AsymmetricEncrypt.spec.ts | 45 + .../UseCase/Asymmetric/AsymmetricEncrypt.ts | 45 + ...AsymmetricSignatureVerificationDetached.ts | 36 + .../004/UseCase/Hash/DeriveHashingKey.ts | 28 + .../Operator/004/UseCase/Hash/HashString.ts | 10 + .../Operator/004/UseCase/Hash/HashingKey.ts | 3 + .../KeySystem/CreateKeySystemItemsKey.ts | 45 + .../KeySystem/CreateRandomKeySystemRootKey.ts | 35 + .../CreateUserInputKeySystemRootKey.ts | 34 + .../KeySystem/DeriveKeySystemRootKey.ts | 60 ++ .../004/UseCase/RootKey/CreateRootKey.ts | 29 + .../004/UseCase/RootKey/DeriveRootKey.ts | 66 ++ .../GenerateAuthenticatedData.spec.ts | 111 +++ .../Symmetric/GenerateAuthenticatedData.ts | 58 ++ .../GenerateDecryptedParameters.spec.ts | 80 ++ .../Symmetric/GenerateDecryptedParameters.ts | 140 +++ .../GenerateEncryptedParameters.spec.ts | 137 +++ .../Symmetric/GenerateEncryptedParameters.ts | 120 +++ .../GenerateEncryptedProtocolString.spec.ts | 44 + .../GenerateEncryptedProtocolString.ts | 41 + .../GenerateSymmetricAdditionalData.spec.ts | 43 + .../GenerateSymmetricAdditionalData.ts | 37 + ...ateSymmetricPayloadSignatureResult.spec.ts | 303 +++++++ ...GenerateSymmetricPayloadSignatureResult.ts | 127 +++ .../GetPayloadAuthenticatedDataDetached.ts | 27 + .../CreateConsistentBase64JsonPayload.ts | 10 + .../Utils/ParseConsistentBase64JsonPayload.ts | 9 + .../Utils/StringToAuthenticatedData.ts | 19 + .../Operator/004/V004AlgorithmHelpers.ts | 20 + .../Domain/Operator/004/V004AlgorithmTypes.ts | 32 + .../Domain/Operator/005/Operator005.spec.ts | 75 -- .../src/Domain/Operator/005/Operator005.ts | 80 -- .../src/Domain/Operator/Functions.ts | 13 +- .../src/Domain/Operator/Operator.ts | 86 -- .../AsyncOperatorInterface.ts | 22 + .../OperatorInterface/OperatorInterface.ts | 102 +++ .../SyncOperatorInterface.ts | 31 + .../Operator/OperatorInterface/TypeCheck.ts | 13 + .../src/Domain/Operator/OperatorManager.ts | 8 +- .../src/Domain/Operator/OperatorWrapper.ts | 32 +- .../Operator/Types/AsymmetricDecryptResult.ts | 8 + ...tricSignatureVerificationDetachedResult.ts | 9 + .../src/Domain/Operator/Types/PublicKeySet.ts | 4 + .../src/Domain/Operator/Types/Types.ts | 4 + .../Encryption/EncryptionProviderInterface.ts | 101 ++- .../Service/KeySystemKeyManagerInterface.ts | 31 + .../src/Domain/Split/AbstractKeySplit.ts | 11 +- .../src/Domain/Split/EncryptionSplit.ts | 32 + .../src/Domain/Split/EncryptionTypeSplit.ts | 1 + .../encryption/src/Domain/Split/Functions.ts | 14 +- .../src/Domain/Types/DecryptedParameters.ts | 7 + .../src/Domain/Types/EncryptedParameters.ts | 28 +- .../Domain/Types/EncryptionAdditionalData.ts | 15 + .../src/Domain/Types/ItemAuthenticatedData.ts | 8 +- .../KeySystemItemsKeyAuthenticatedData.ts | 7 + ...SystemRootKeyEncryptedAuthenticatedData.ts | 4 + packages/encryption/src/Domain/index.ts | 22 +- packages/encryption/tsconfig.json | 10 +- packages/features/package.json | 2 +- packages/filepicker/package.json | 2 +- packages/files/package.json | 2 +- .../src/Domain/Api/DownloadFileParams.ts | 11 + .../files/src/Domain/Api/FileOwnershipType.ts | 1 + .../files/src/Domain/Api/FilesApiInterface.ts | 52 +- .../Operations/DownloadAndDecrypt.spec.ts | 28 +- .../Domain/Operations/DownloadAndDecrypt.ts | 5 +- .../src/Domain/Operations/EncryptAndUpload.ts | 16 +- .../Domain/Service/FilesClientInterface.ts | 22 +- .../src/Domain/UseCase/FileDownloader.spec.ts | 24 +- .../src/Domain/UseCase/FileDownloader.ts | 28 +- .../src/Domain/UseCase/FileUploader.spec.ts | 4 +- .../files/src/Domain/UseCase/FileUploader.ts | 10 +- packages/files/src/Domain/index.ts | 3 + packages/icons/src/Icons/ic-group.svg | 86 ++ packages/icons/src/Icons/index.ts | 2 + packages/mobile/ios/Podfile.lock | 4 +- packages/mobile/ios/StandardNotes/Info.plist | 2 +- packages/mobile/src/Lib/Database/Database.ts | 26 +- packages/models/package.json | 3 +- .../Abstract/Contextual/ContextPayload.ts | 5 + .../Abstract/Contextual/FilteredServerItem.ts | 2 +- .../Domain/Abstract/Contextual/Functions.ts | 4 + .../Abstract/Contextual/LocalStorage.ts | 19 + .../Abstract/Contextual/ServerSyncPush.ts | 4 + .../Abstract/Contextual/ServerSyncSaved.ts | 10 + .../Contextual/TrustedConflictParams.ts | 4 + .../Item/Implementations/DecryptedItem.ts | 2 +- .../Item/Implementations/GenericItem.ts | 21 + .../Abstract/Item/Interfaces/DecryptedItem.ts | 2 +- .../Abstract/Item/Interfaces/ItemInterface.ts | 6 + .../Item/Mutator/DecryptedItemMutator.ts | 12 +- .../Abstract/Item/Mutator/ItemMutator.ts | 22 + .../Payload/Implementations/PurePayload.ts | 32 +- .../Payload/Interfaces/PayloadInterface.ts | 7 + .../{Types => Overrides}/TimestampDefaults.ts | 0 .../Payload/Overrides/VaultOverride.ts | 15 + .../Abstract/Payload/Types/PayloadSource.ts | 2 + .../src/Domain/Abstract/Payload/index.ts | 3 +- .../Interfaces/TransferPayload.ts | 8 + .../KeySystemRootKeyParamsInterface.ts | 16 + .../KeyParams/KeySystemRootKeyPasswordType.ts | 4 + .../src/Domain/Local/RootKey/KeychainTypes.ts | 3 + .../Domain/Local/RootKey/RootKeyContent.ts | 4 + .../Domain/Local/RootKey/RootKeyInterface.ts | 6 + .../RootKey/RootKeyWithKeyPairsInterface.ts | 7 + .../AsymmetricMessageDataCommon.ts | 3 + .../AsymmetricMessagePayload.ts | 12 + .../AsymmetricMessagePayloadType.ts | 7 + .../AsymmetricMessageSenderKeypairChanged.ts | 10 + .../AsymmetricMessageSharedVaultInvite.ts | 16 + ...metricMessageSharedVaultMetadataChanged.ts | 11 + ...mmetricMessageSharedVaultRootKeyChanged.ts | 8 + .../AsymmetricMessageTrustedContactShare.ts | 8 + ...ItemsIndex.spec.ts => ItemCounter.spec.ts} | 6 +- .../Item/{TagItemsIndex.ts => ItemCounter.ts} | 74 +- .../Runtime/Deltas/RemoteDataConflicts.ts | 13 +- .../Runtime/Deltas/RemoteRejected.spec.ts | 16 +- .../Domain/Runtime/Deltas/RemoteRejected.ts | 220 ++++- .../Domain/Runtime/Deltas/RemoteRetrieved.ts | 2 +- .../src/Domain/Runtime/Deltas/RemoteSaved.ts | 2 +- .../Runtime/Deltas/RemoteUuidConflicts.ts | 11 +- .../Runtime/Display/DisplayOptions.spec.ts | 22 +- .../Domain/Runtime/Display/DisplayOptions.ts | 25 +- .../Display/DisplayOptionsToFilters.ts | 6 +- .../Runtime/Display/ItemDisplayController.ts | 59 +- .../Runtime/Display/Search/SearchUtilities.ts | 6 +- .../Validator/CollectionCriteriaValidator.ts | 11 + .../Validator/CriteriaValidatorInterface.ts | 3 + .../CustomFilterCriteriaValidator.ts | 11 + .../ExcludeVaultsCriteriaValidator.ts | 15 + .../ExclusiveVaultCriteriaValidator.ts | 11 + .../HiddenContentCriteriaValidator.ts | 11 + .../Runtime/Display/VaultDisplayOptions.ts | 109 +++ .../Display/VaultDisplayOptionsTypes.ts | 12 + .../src/Domain/Runtime/Display/index.ts | 2 + ...ntentTypeUsesKeySystemRootKeyEncryption.ts | 5 + .../ContentTypeUsesRootKeyEncryption.ts | 6 + .../ContentTypesUsingRootKeyEncryption.ts | 11 + .../Encryption/PersistentSignatureData.ts | 19 + .../KeySystemItemsKeyContent.ts | 11 + .../KeySystemItemsKeyInterface.ts | 11 + .../KeySystemItemsKeyMutatorInterface.ts | 4 + .../KeySystemRootKey/KeySystemIdentifier.ts | 1 + .../KeySystemRootKey/KeySystemRootKey.ts | 54 ++ .../KeySystemRootKeyContent.ts | 16 + .../KeySystemRootKeyInterface.ts | 38 + .../KeySystemRootKeyMutator.ts | 4 + .../KeySystemRootKeyStorageMode.ts | 5 + .../Syncable/SmartView/SmartViewBuilder.ts | 18 +- .../PublicKeySet/ContactPublicKeySet.ts | 73 ++ .../ContactPublicKeySetInterface.ts | 15 + .../ContactPublicKeySetJsonInterface.ts | 7 + .../PublicKeySet/FindPublicKeySetResult.ts | 8 + .../Syncable/TrustedContact/TrustedContact.ts | 77 ++ .../TrustedContact/TrustedContactContent.ts | 11 + .../TrustedContact/TrustedContactInterface.ts | 16 + .../TrustedContact/TrustedContactMutator.ts | 26 + .../Syncable/VaultListing/VaultListing.ts | 62 ++ .../VaultListing/VaultListingContent.ts | 19 + .../VaultListing/VaultListingInterface.ts | 29 + .../VaultListing/VaultListingMutator.ts | 27 + .../VaultListing/VaultListingSharingInfo.ts | 4 + .../Domain/Utilities/Item/ItemGenerator.ts | 21 +- .../src/Domain/Utilities/Test/SpecUtils.ts | 4 +- packages/models/src/Domain/index.ts | 49 +- packages/responses/package.json | 2 +- .../AsymmetricMessageServerHash.ts | 8 + .../Domain/Error/ClientDisplayableError.ts | 23 + .../responses/src/Domain/Error/ClientError.ts | 9 - .../Domain/Files/CreateValetTokenPayload.ts | 4 +- .../src/Domain/Files/MoveFileResponse.ts | 3 + .../src/Domain/Files/ValetTokenOperation.ts | 1 + .../responses/src/Domain/Http/ErrorTag.ts | 1 + .../responses/src/Domain/Http/HttpResponse.ts | 2 +- .../src/Domain/Item/ApiEndpointParam.ts | 1 + .../src/Domain/Item/ConflictParams.ts | 99 +- .../responses/src/Domain/Item/ConflictType.ts | 6 +- .../responses/src/Domain/Item/RawSyncData.ts | 8 + .../src/Domain/Item/ServerItemResponse.ts | 4 + .../SharedVaultInviteServerHash.ts | 13 + .../SharedVaults/SharedVaultPermission.ts | 5 + .../SharedVaults/SharedVaultServerHash.ts | 4 + .../SharedVaults/SharedVaultUserServerHash.ts | 9 + .../src/Domain/UserEvent/UserEventPayload.ts | 14 + .../Domain/UserEvent/UserEventServerHash.ts | 10 + .../src/Domain/UserEvent/UserEventType.ts | 4 + packages/responses/src/Domain/index.ts | 21 +- packages/services/package.json | 6 +- .../services/src/Domain/Alert/AlertService.ts | 10 + .../Application/ApplicationInterface.ts | 49 +- .../AsymmetricMessageService.spec.ts | 63 ++ .../AsymmetricMessageService.ts | 187 ++++ .../GetAsymmetricMessageTrustedPayload.ts | 23 + .../GetAsymmetricMessageUntrustedPayload.ts | 17 + .../UseCase/GetInboundAsymmetricMessages.ts | 16 + .../UseCase/GetOutboundAsymmetricMessages.ts | 16 + ...dleTrustedSharedVaultInviteMessage.spec.ts | 67 ++ .../HandleTrustedSharedVaultInviteMessage.ts | 64 ++ ...TrustedSharedVaultRootKeyChangedMessage.ts | 44 + .../UseCase/SendAsymmetricMessageUseCase.ts | 24 + .../UseCase/SendOwnContactChangeMessage.ts | 47 + .../src/Domain/Backups/BackupService.spec.ts | 5 +- .../src/Domain/Backups/BackupService.ts | 6 +- .../Challenge/ChallengeServiceInterface.ts | 12 +- .../Challenge/Types/ChallengeObserver.ts | 10 + .../Challenge/Types/ChallengeValueCallback.ts | 3 + .../services/src/Domain/Challenge/index.ts | 2 + .../Component/ComponentManagerInterface.ts | 2 + .../src/Domain/Contacts/CollaborationID.ts | 8 + .../src/Domain/Contacts/ContactService.ts | 264 ++++++ .../Contacts/ContactServiceInterface.ts | 43 + .../Contacts/Managers/SelfContactManager.ts | 129 +++ .../src/Domain/Contacts/UnknownContactName.ts | 1 + .../UseCase/CreateOrEditTrustedContact.ts | 61 ++ .../Contacts/UseCase/FindContactQuery.ts | 1 + .../Contacts/UseCase/FindTrustedContact.ts | 29 + .../Contacts/UseCase/UpdateTrustedContact.ts | 32 + .../UseCase/ValidateItemSigner.spec.ts | 347 +++++++ .../Contacts/UseCase/ValidateItemSigner.ts | 122 +++ .../UseCase/ValidateItemSignerResult.ts | 1 + .../src/Domain/Device/DatabaseLoadOptions.ts | 4 + .../src/Domain/Device/DatabaseLoadSorter.ts | 12 +- .../Domain/Encryption/BackupFileDecryptor.ts | 251 ------ .../Encryption/DecryptBackupFileUseCase.ts | 282 ++++++ .../Domain/Encryption/EncryptionService.ts | 379 ++++++-- .../src/Domain/Encryption/Functions.ts | 2 +- .../src/Domain/Encryption/ItemsEncryption.ts | 105 ++- .../Domain/Encryption/RootKeyEncryption.ts | 201 +++-- .../src/Domain/Event/ApplicationEvent.ts | 83 +- .../services/src/Domain/Event/SyncEvent.ts | 20 +- .../src/Domain/Files/FileService.spec.ts | 39 +- .../services/src/Domain/Files/FileService.ts | 173 +++- .../InternalFeatures/InternalFeature.ts | 3 + .../InternalFeatureService.ts | 24 + .../InternalFeatureServiceInterface.ts | 6 + .../src/Domain/Item/ItemCounterInterface.ts | 5 - .../src/Domain/Item/ItemManagerInterface.ts | 91 +- .../src/Domain/Item/ItemsClientInterface.ts | 174 ---- ...nter.spec.ts => StaticItemCounter.spec.ts} | 4 +- .../{ItemCounter.ts => StaticItemCounter.ts} | 4 +- .../Domain/KeySystem/KeySystemKeyManager.ts | 158 ++++ .../src/Domain/Mutator/ImportDataUseCase.ts | 146 +++ .../Domain/Mutator/MutatorClientInterface.ts | 195 ++-- .../Payloads/PayloadManagerInterface.ts | 2 + .../Protection/ProtectionClientInterface.ts | 11 +- .../Revision/RevisionClientInterface.ts | 16 +- .../src/Domain/Revision/RevisionManager.ts | 16 +- .../src/Domain/Revision/RevisionPayload.ts | 14 + .../src/Domain/Service/AbstractService.ts | 9 +- .../src/Domain/Session/SessionEvent.ts | 5 + .../Domain/Session/SessionsClientInterface.ts | 10 +- .../Session/UserKeyPairChangedEventData.ts | 9 + .../PendingSharedVaultInviteRecord.ts | 8 + .../Domain/SharedVaults/SharedVaultService.ts | 587 ++++++++++++ .../SharedVaults/SharedVaultServiceEvent.ts | 10 + .../SharedVaultServiceInterface.ts | 55 ++ .../UseCase/AcceptTrustedSharedVaultInvite.ts | 29 + .../UseCase/ConvertToSharedVault.ts | 47 + .../SharedVaults/UseCase/CreateSharedVault.ts | 64 ++ .../UseCase/DeleteExternalSharedVault.ts | 48 + .../SharedVaults/UseCase/DeleteSharedVault.ts | 33 + .../UseCase/GetSharedVaultTrustedContacts.ts | 23 + .../UseCase/GetSharedVaultUsers.ts | 16 + .../UseCase/InviteContactToSharedVault.ts | 63 ++ .../SharedVaults/UseCase/LeaveSharedVault.ts | 48 + ...NotifySharedVaultUsersOfRootKeyRotation.ts | 62 ++ .../UseCase/RemoveSharedVaultMember.ts | 17 + ...ploadSharedVaultInvitesAfterKeyRotation.ts | 144 +++ .../UseCase/SendSharedVaultInviteUseCase.ts | 31 + ...dSharedVaultMetadataChangedMessageToAll.ts | 100 +++ ...ndSharedVaultRootKeyChangedMessageToAll.ts | 103 +++ ...ShareContactWithAllMembersOfSharedVault.ts | 78 ++ .../UseCase/UpdateSharedVaultInvite.ts | 31 + .../Singleton/SingletonManagerInterface.ts | 26 + .../src/Domain/Storage/StorageKeys.ts | 1 + .../Domain/Storage/StorageServiceInterface.ts | 6 +- .../src/Domain/Strings/InfoStrings.ts | 5 +- .../services/src/Domain/Strings/Messages.ts | 2 + .../services/src/Domain/Sync/SyncOptions.ts | 6 + .../src/Domain/Sync/SyncServiceInterface.ts | 5 +- .../src/Domain/User/UserClientInterface.ts | 49 +- .../services/src/Domain/User/UserService.ts | 66 +- .../src/Domain/UserEvent/UserEventService.ts | 38 + .../Domain/UserEvent/UserEventServiceEvent.ts | 9 + .../Domain/Vaults/ChangeVaultOptionsDTO.ts | 10 + .../Vaults/UseCase/ChangeVaultKeyOptions.ts | 150 ++++ .../src/Domain/Vaults/UseCase/CreateVault.ts | 115 +++ .../src/Domain/Vaults/UseCase/DeleteVault.ts | 32 + .../src/Domain/Vaults/UseCase/GetVault.ts | 17 + .../Domain/Vaults/UseCase/MoveItemsToVault.ts | 42 + .../Vaults/UseCase/RemoveItemFromVault.ts | 26 + .../Vaults/UseCase/RotateVaultRootKey.ts | 90 ++ .../src/Domain/Vaults/VaultService.ts | 322 +++++++ .../src/Domain/Vaults/VaultServiceEvent.ts | 19 + .../Domain/Vaults/VaultServiceInterface.ts | 47 + packages/services/src/Domain/index.ts | 65 +- packages/services/tsconfig.json | 3 +- .../src/Common/PureCryptoInterface.ts | 11 +- .../sncrypto-common/src/Types/PkcKeyPair.ts | 1 - .../src/Types/SodiumConstant.ts | 3 + packages/sncrypto-web/src/crypto.ts | 54 +- packages/sncrypto-web/src/libsodium.ts | 8 + packages/sncrypto-web/test/crypto.test.js | 16 +- packages/snjs/README.md | 24 +- packages/snjs/jest-global.ts | 1 + packages/snjs/lib/Application/Application.ts | 205 ++++- packages/snjs/lib/Application/Event.ts | 4 +- packages/snjs/lib/Application/LiveItem.ts | 4 +- .../Domain/UseCase/GetRevision/GetRevision.ts | 5 +- packages/snjs/lib/IsDev.ts | 3 + .../Applicators/TagsToFolders.spec.ts | 72 +- .../Migrations/Applicators/TagsToFolders.ts | 7 +- packages/snjs/lib/Migrations/Base.ts | 8 +- .../snjs/lib/Migrations/MigrationServices.ts | 8 +- .../snjs/lib/Migrations/Versions/2_20_0.ts | 2 +- .../snjs/lib/Migrations/Versions/2_36_0.ts | 2 +- .../snjs/lib/Migrations/Versions/2_42_0.ts | 2 +- .../snjs/lib/Migrations/Versions/2_7_0.ts | 2 +- packages/snjs/lib/Services/Api/ApiService.ts | 147 +-- packages/snjs/lib/Services/Api/Paths.ts | 13 +- .../Services/Challenge/ChallengeOperation.ts | 7 +- .../Services/Challenge/ChallengeService.ts | 25 +- .../ComponentManager/ComponentManager.spec.ts | 15 +- .../ComponentManager/ComponentManager.ts | 17 +- .../ComponentManager/ComponentViewer.ts | 14 +- .../Services/Features/FeaturesService.spec.ts | 37 +- .../lib/Services/Features/FeaturesService.ts | 26 +- .../lib/Services/Items/ItemManager.spec.ts | 308 +++---- .../snjs/lib/Services/Items/ItemManager.ts | 696 ++------------ .../KeyRecovery/KeyRecoveryService.ts | 2 +- .../snjs/lib/Services/Listed/ListedService.ts | 10 +- .../Services/Mutator/MutatorService.spec.ts | 113 ++- .../lib/Services/Mutator/MutatorService.ts | 847 ++++++++++++------ .../lib/Services/Payloads/PayloadManager.ts | 2 +- .../Preferences/PreferencesService.ts | 8 +- .../Protection/ProtectionService.spec.ts | 6 +- .../Services/Protection/ProtectionService.ts | 84 +- .../lib/Services/Session/SessionManager.ts | 138 ++- .../Services/Singleton/SingletonManager.ts | 76 +- .../Services/Storage/DiskStorageService.ts | 60 +- .../lib/Services/Sync/Account/Operation.ts | 28 +- .../lib/Services/Sync/Account/Response.ts | 122 +-- .../Services/Sync/Account/ResponseResolver.ts | 47 +- .../Sync/Account/ServerConflictMap.ts | 5 + .../snjs/lib/Services/Sync/SyncService.ts | 367 +++++--- packages/snjs/lib/Strings/Input.ts | 3 - packages/snjs/lib/Strings/index.ts | 2 - packages/snjs/lib/index.ts | 1 + packages/snjs/lib/tsconfig.json | 6 +- packages/snjs/mocha/000.test.js | 2 +- packages/snjs/mocha/004.test.js | 75 +- packages/snjs/mocha/TestRegistry/BaseTests.js | 58 ++ .../snjs/mocha/TestRegistry/MainRegistry.js | 7 + .../snjs/mocha/TestRegistry/VaultTests.js | 16 + packages/snjs/mocha/actions.test.js | 17 +- packages/snjs/mocha/application.test.js | 10 +- packages/snjs/mocha/auth-fringe-cases.test.js | 6 +- packages/snjs/mocha/auth.test.js | 6 +- packages/snjs/mocha/backups.test.js | 29 +- packages/snjs/mocha/features.test.js | 24 +- packages/snjs/mocha/files.test.js | 44 +- packages/snjs/mocha/history.test.js | 49 +- packages/snjs/mocha/item_manager.test.js | 467 +++------- .../snjs/mocha/key_recovery_service.test.js | 21 +- packages/snjs/mocha/keys.test.js | 60 +- packages/snjs/mocha/lib/AppContext.js | 285 +++++- packages/snjs/mocha/lib/Applications.js | 4 - packages/snjs/mocha/lib/BaseItemCounts.js | 35 + packages/snjs/mocha/lib/Collaboration.js | 140 +++ packages/snjs/mocha/lib/Events.js | 19 + packages/snjs/mocha/lib/Files.js | 14 +- packages/snjs/mocha/lib/Items.js | 2 +- packages/snjs/mocha/lib/factory.js | 49 +- packages/snjs/mocha/lib/fake_web_crypto.js | 35 +- .../snjs/mocha/lib/web_device_interface.js | 32 +- .../snjs/mocha/migrations/migration.test.js | 2 +- .../snjs/mocha/model_tests/appmodels.test.js | 54 +- .../snjs/mocha/model_tests/importing.test.js | 100 ++- packages/snjs/mocha/model_tests/items.test.js | 38 +- .../snjs/mocha/model_tests/mapping.test.js | 30 +- .../snjs/mocha/model_tests/notes_tags.test.js | 115 +-- .../model_tests/notes_tags_folders.test.js | 4 +- .../mocha/model_tests/performance.test.js | 4 +- packages/snjs/mocha/mutator.test.js | 2 +- packages/snjs/mocha/mutator_service.test.js | 271 ++++++ .../snjs/mocha/note_display_criteria.test.js | 195 ++-- packages/snjs/mocha/protection.test.js | 44 +- packages/snjs/mocha/session.test.js | 2 +- packages/snjs/mocha/settings.test.js | 66 +- packages/snjs/mocha/singletons.test.js | 14 +- packages/snjs/mocha/storage.test.js | 4 +- packages/snjs/mocha/subscriptions.test.js | 3 +- .../snjs/mocha/sync_tests/conflicting.test.js | 78 +- .../snjs/mocha/sync_tests/integrity.test.js | 8 +- .../snjs/mocha/sync_tests/notes_tags.test.js | 14 +- .../snjs/mocha/sync_tests/offline.test.js | 17 +- packages/snjs/mocha/sync_tests/online.test.js | 98 +- packages/snjs/mocha/test.html | 104 +-- packages/snjs/mocha/upgrading.test.js | 2 +- .../mocha/vaults/asymmetric-messages.test.js | 277 ++++++ packages/snjs/mocha/vaults/conflicts.test.js | 186 ++++ packages/snjs/mocha/vaults/contacts.test.js | 83 ++ packages/snjs/mocha/vaults/crypto.test.js | 204 +++++ packages/snjs/mocha/vaults/deletion.test.js | 159 ++++ packages/snjs/mocha/vaults/files.test.js | 259 ++++++ packages/snjs/mocha/vaults/invites.test.js | 229 +++++ packages/snjs/mocha/vaults/items.test.js | 121 +++ .../snjs/mocha/vaults/key_rotation.test.js | 269 ++++++ .../snjs/mocha/vaults/permissions.test.js | 133 +++ packages/snjs/mocha/vaults/pkc.test.js | 98 ++ .../snjs/mocha/vaults/shared_vaults.test.js | 114 +++ packages/snjs/mocha/vaults/vaults.test.js | 250 ++++++ packages/snjs/package.json | 4 +- packages/snjs/webpack.dev.js | 12 +- packages/snjs/webpack.prod.js | 12 +- packages/ui-services/package.json | 7 +- .../src/Abstract/AbstractUIService.ts | 52 ++ .../Abstract/AbstractUIServiceInterface.ts | 7 + .../ui-services/src/Alert/WebAlertService.ts | 24 + .../AegisToAuthenticatorConverter.spec.ts | 2 +- .../AegisToAuthenticatorConverter.ts | 2 +- .../EvernoteConverter.spec.ts | 2 +- .../EvernoteConverter/EvernoteConverter.ts | 2 +- .../GoogleKeepConverter.spec.ts | 2 +- .../GoogleKeepConverter.ts | 2 +- packages/ui-services/src/Import/Importer.ts | 5 +- .../PlaintextConverter/PlaintextConverter.ts | 2 +- .../SimplenoteConverter.spec.ts | 2 +- .../SimplenoteConverter.ts | 2 +- .../src/Keyboard/getKeyboardShortcuts.ts | 2 +- .../src/Preferences/PreferenceId.ts | 1 + .../ui-services/src/Theme/ThemeManager.ts | 62 +- .../src/Vaults/VaultDisplayService.ts | 216 +++++ .../src/Vaults/VaultDisplayServiceEvent.ts | 3 + .../Vaults/VaultDisplayServiceInterface.ts | 20 + .../WebApplicationInterface.ts | 10 +- packages/ui-services/src/index.ts | 6 + packages/utils/package.json | 2 +- packages/web/package.json | 2 +- .../Application/Device/DesktopManager.ts | 4 +- .../Application/Device/WebOrDesktopDevice.ts | 29 +- .../javascripts/Application/WebApplication.ts | 20 +- .../javascripts/Application/WebServices.ts | 2 + .../ChangeEditor/ChangeEditorMenu.tsx | 6 +- .../ChangeEditor/ChangeMultipleMenu.tsx | 6 +- .../ClipperView/ClippedNoteView.tsx | 3 +- .../Components/ClipperView/ClipperView.tsx | 3 +- .../ContentListView/FileListItem.tsx | 2 + .../ContentListView/FileListItemCard.tsx | 2 + .../Header/ContentListHeader.tsx | 6 +- .../Header/DisplayOptionsMenu.tsx | 6 +- .../ContentListView/ListItemVaultInfo.tsx | 46 + .../ContentListView/NoteListItem.tsx | 2 + .../ContentTableView/ContentTableView.tsx | 6 +- .../FileContextMenu/FileMenuOptions.tsx | 4 + .../FilePreview/FilePreviewModal.tsx | 4 +- .../FileView/FileViewWithoutProtection.tsx | 2 +- .../javascripts/Components/Footer/Footer.tsx | 18 + .../Components/Footer/QuickSettingsButton.tsx | 2 +- .../Footer/VaultSelectionButton.tsx | 62 ++ .../Components/Icon/IconNameToSvgMapping.tsx | 2 + .../NoteView/CollaborationInfoHUD.tsx | 46 + .../Controller/NoteViewController.spec.ts | 18 +- .../NoteView/Controller/NoteViewController.ts | 15 +- .../TemplateNoteViewControllerOptions.ts | 3 +- .../NoteConflictResolutionModal.tsx | 3 +- .../Components/NoteView/NoteView.tsx | 15 +- .../NoteView/NoteViewFileDropTarget.tsx | 4 +- .../NoteView/ReadonlyNoteContent.tsx | 4 +- .../Components/NotesOptions/NotesOptions.tsx | 8 +- .../Components/Preferences/PaneSelector.tsx | 3 + .../Preferences/Panes/Backups/DataBackups.tsx | 2 +- .../Advanced/Packages/PackageEntry.tsx | 4 +- .../General/Advanced/Packages/Section.tsx | 1 + .../Preferences/Panes/General/Persistence.tsx | 4 +- .../EditSmartViewModalController.tsx | 7 +- .../Panes/General/SmartViews/SmartViews.tsx | 7 +- .../Panes/Security/EncryptionEnabled.tsx | 4 +- .../Panes/Security/ErroredItems.tsx | 6 +- .../Preferences/Panes/Security/Security.tsx | 2 +- .../Panes/Security/securityPrefsHasBubble.tsx | 2 +- .../Panes/Vaults/Contacts/ContactItem.tsx | 51 ++ .../Vaults/Contacts/EditContactModal.tsx | 127 +++ .../Vaults/Invites/ContactInviteModal.tsx | 97 ++ .../Panes/Vaults/Invites/InviteItem.tsx | 67 ++ .../Preferences/Panes/Vaults/Vaults.tsx | 157 ++++ .../Panes/Vaults/Vaults/VaultItem.tsx | 146 +++ .../Vaults/VaultModal/EditVaultModal.tsx | 224 +++++ .../VaultModal/KeyStoragePreference.tsx | 57 ++ .../VaultModal/PasswordTypePreference.tsx | 75 ++ .../Vaults/VaultModal/VaultModalInvites.tsx | 57 ++ .../Vaults/VaultModal/VaultModalMembers.tsx | 71 ++ .../Components/Preferences/PreferencesMenu.ts | 6 + .../QuickSettingsMenu/QuickSettingsMenu.tsx | 6 +- .../QuickSettingsMenu/ThemesMenuButton.tsx | 2 +- .../RadioButtonGroup/RadioButtonGroup.tsx | 5 +- .../AddSmartViewModalController.ts | 8 +- .../Plugins/EncryptedFilePlugin/FilePlugin.ts | 4 +- .../SuperEditor/SuperNoteConverter.tsx | 4 +- .../Components/Tags/Navigation.tsx | 11 + .../Components/Tags/TagContextMenu.tsx | 7 + .../Components/Tags/TagsSection.tsx | 1 + .../ManyVaultSelectionMenu.tsx | 52 ++ .../SingleVaultSelectionMenu.tsx | 41 + .../VaultSelectionMenu/VaultSelectionMenu.tsx | 51 ++ .../Vaults/AddToVaultMenuOption.tsx | 155 ++++ .../Components/Vaults/VaultNameBadge.tsx | 20 + .../Abstract/PersistenceService.ts | 4 +- .../AccountMenu/AccountMenuController.ts | 4 +- .../Controllers/FeaturesController.ts | 4 +- .../Controllers/FilesController.ts | 27 +- .../Controllers/ImportModalController.ts | 2 +- .../ItemList/ItemListController.spec.ts | 3 - .../ItemList/ItemListController.ts | 24 +- .../Controllers/LinkingController.spec.ts | 71 +- .../Controllers/LinkingController.tsx | 82 +- .../Navigation/NavigationController.ts | 96 +- .../Controllers/Navigation/TagsCountsState.ts | 24 + .../Controllers/NoAccountWarningController.ts | 4 +- .../NoteHistory/HistoryModalController.ts | 8 +- .../NoteHistory/NoteHistoryController.ts | 6 +- .../Controllers/NoteSyncController.ts | 4 +- .../NotesController/NotesController.ts | 26 +- .../PaneController/PaneController.ts | 4 +- .../Controllers/PreferencesController.ts | 4 +- .../PurchaseFlow/PurchaseFlowController.ts | 4 +- .../Controllers/QuickSettingsController.ts | 4 +- .../Controllers/SearchOptionsController.ts | 4 +- .../Controllers/SelectedItemsController.ts | 4 +- .../Subscription/SubscriptionController.ts | 6 +- .../VaultSelectionMenuController.ts | 47 + .../Controllers/ViewControllerManager.ts | 49 +- packages/web/src/javascripts/FeatureTrunk.ts | 16 +- packages/web/src/javascripts/Hooks/useItem.ts | 29 + .../NativeMobileWeb/MobileWebReceiver.ts | 3 +- .../Utils/Dev/PurchaseMockSubscription.ts | 32 + .../Items/Display/getTitleForLinkedTag.ts | 3 +- .../Utils/Items/Icons/getIconForItem.ts | 3 +- .../Items/Search/doesItemMatchSearchQuery.ts | 3 +- .../Utils/Items/Search/getSearchResults.ts | 3 +- yarn.lock | 162 +++- 638 files changed, 20321 insertions(+), 4813 deletions(-) create mode 100644 .yarn/cache/@standardnotes-common-npm-1.48.3-83aea658e0-191d97879d.zip create mode 100644 .yarn/cache/@typescript-eslint-eslint-plugin-npm-5.59.9-3d09cd2e8f-bd2428e307.zip create mode 100644 .yarn/cache/@typescript-eslint-parser-npm-5.59.9-3841845448-69b07d0a5b.zip create mode 100644 .yarn/cache/@typescript-eslint-scope-manager-npm-5.59.9-c9c714cb56-362c22662d.zip create mode 100644 .yarn/cache/@typescript-eslint-type-utils-npm-5.59.9-fc3a85cbad-6bc2619c50.zip create mode 100644 .yarn/cache/@typescript-eslint-types-npm-5.59.9-9719f93248-283f8fee1e.zip create mode 100644 .yarn/cache/@typescript-eslint-typescript-estree-npm-5.59.9-ec2ce6608c-c0c9b81f20.zip create mode 100644 .yarn/cache/@typescript-eslint-utils-npm-5.59.9-d1ab6a9f9a-22ec596288.zip create mode 100644 .yarn/cache/@typescript-eslint-visitor-keys-npm-5.59.9-3e52021052-2909ce761f.zip delete mode 100644 packages/api/src/Domain/Request/ApiEndpointParam.ts create mode 100644 packages/api/src/Domain/Request/AsymmetricMessage/CreateAsymmetricMessageParams.ts create mode 100644 packages/api/src/Domain/Request/AsymmetricMessage/DeleteAsymmetricMessageRequestParams.ts create mode 100644 packages/api/src/Domain/Request/AsymmetricMessage/GetOutboundAsymmetricMessagesRequestParams.ts create mode 100644 packages/api/src/Domain/Request/AsymmetricMessage/GetUserAsymmetricMessagesRequestParams.ts create mode 100644 packages/api/src/Domain/Request/AsymmetricMessage/UpdateAsymmetricMessageParams.ts create mode 100644 packages/api/src/Domain/Request/SharedVault/CreateSharedVaultValetTokenParams.ts create mode 100644 packages/api/src/Domain/Request/SharedVault/SharedVaultMoveType.ts create mode 100644 packages/api/src/Domain/Request/SharedVaultInvites/AcceptInviteRequestParams.ts create mode 100644 packages/api/src/Domain/Request/SharedVaultInvites/CreateSharedVaultInviteParams.ts create mode 100644 packages/api/src/Domain/Request/SharedVaultInvites/DeclineInviteRequestParams.ts create mode 100644 packages/api/src/Domain/Request/SharedVaultInvites/DeleteAllSharedVaultInvitesRequestParams.ts create mode 100644 packages/api/src/Domain/Request/SharedVaultInvites/DeleteInviteRequestParams.ts create mode 100644 packages/api/src/Domain/Request/SharedVaultInvites/GetOutboundUserInvitesRequestParams.ts create mode 100644 packages/api/src/Domain/Request/SharedVaultInvites/GetSharedVaultInvitesRequestParams.ts create mode 100644 packages/api/src/Domain/Request/SharedVaultInvites/GetUserInvitesRequestParams.ts create mode 100644 packages/api/src/Domain/Request/SharedVaultInvites/UpdateSharedVaultInviteParams.ts create mode 100644 packages/api/src/Domain/Request/SharedVaultUser/DeleteSharedVaultUserRequestParams.ts create mode 100644 packages/api/src/Domain/Request/SharedVaultUser/GetSharedVaultUsersRequestParams.ts create mode 100644 packages/api/src/Domain/Request/User/UserUpdateRequestParams.ts create mode 100644 packages/api/src/Domain/Response/AsymmetricMessage/CreateAsymmetricMessageResponse.ts create mode 100644 packages/api/src/Domain/Response/AsymmetricMessage/DeleteAsymmetricMessageResponse.ts create mode 100644 packages/api/src/Domain/Response/AsymmetricMessage/GetOutboundAsymmetricMessagesResponse.ts create mode 100644 packages/api/src/Domain/Response/AsymmetricMessage/GetUserAsymmetricMessagesResponse.ts create mode 100644 packages/api/src/Domain/Response/AsymmetricMessage/UpdateAsymmetricMessageResponse.ts create mode 100644 packages/api/src/Domain/Response/SharedVault/CreateSharedVaultResponse.ts create mode 100644 packages/api/src/Domain/Response/SharedVault/CreateSharedVaultValetTokenResponse.ts create mode 100644 packages/api/src/Domain/Response/SharedVault/GetSharedVaultsResponse.ts create mode 100644 packages/api/src/Domain/Response/SharedVaultInvites/AcceptInviteResponse.ts create mode 100644 packages/api/src/Domain/Response/SharedVaultInvites/CreateSharedVaultInviteResponse.ts create mode 100644 packages/api/src/Domain/Response/SharedVaultInvites/DeclineInviteResponse.ts create mode 100644 packages/api/src/Domain/Response/SharedVaultInvites/DeleteAllSharedVaultInvitesResponse.ts create mode 100644 packages/api/src/Domain/Response/SharedVaultInvites/DeleteInviteResponse.ts create mode 100644 packages/api/src/Domain/Response/SharedVaultInvites/GetOutboundUserInvitesResponse.ts create mode 100644 packages/api/src/Domain/Response/SharedVaultInvites/GetSharedVaultInvitesResponse.ts create mode 100644 packages/api/src/Domain/Response/SharedVaultInvites/GetUserInvitesResponse.ts create mode 100644 packages/api/src/Domain/Response/SharedVaultInvites/UpdateSharedVaultInviteResponse.ts create mode 100644 packages/api/src/Domain/Response/SharedVaultUsers/DeleteSharedVaultUserResponse.ts create mode 100644 packages/api/src/Domain/Response/SharedVaultUsers/GetSharedVaultUsersResponse.ts create mode 100644 packages/api/src/Domain/Response/User/UserUpdateResponse.ts create mode 100644 packages/api/src/Domain/Server/AsymmetricMessage/AsymmetricMessageServer.ts create mode 100644 packages/api/src/Domain/Server/AsymmetricMessage/AsymmetricMessageServerInterface.ts create mode 100644 packages/api/src/Domain/Server/AsymmetricMessage/Paths.ts create mode 100644 packages/api/src/Domain/Server/SharedVault/Paths.ts create mode 100644 packages/api/src/Domain/Server/SharedVault/SharedVaultServer.ts create mode 100644 packages/api/src/Domain/Server/SharedVault/SharedVaultServerInterface.ts create mode 100644 packages/api/src/Domain/Server/SharedVaultInvites/Paths.ts create mode 100644 packages/api/src/Domain/Server/SharedVaultInvites/SharedVaultInvitesServer.ts create mode 100644 packages/api/src/Domain/Server/SharedVaultInvites/SharedVaultInvitesServerInterface.ts create mode 100644 packages/api/src/Domain/Server/SharedVaultUsers/Paths.ts create mode 100644 packages/api/src/Domain/Server/SharedVaultUsers/SharedVaultUsersServer.ts create mode 100644 packages/api/src/Domain/Server/SharedVaultUsers/SharedVaultUsersServerInterface.ts create mode 100644 packages/encryption/src/Domain/Keys/KeySystemItemsKey/KeySystemItemsKey.ts create mode 100644 packages/encryption/src/Domain/Keys/KeySystemItemsKey/KeySystemItemsKeyMutator.ts create mode 100644 packages/encryption/src/Domain/Keys/KeySystemItemsKey/Registration.ts create mode 100644 packages/encryption/src/Domain/Operator/004/MockedCrypto.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricDecrypt.spec.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricDecrypt.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricEncrypt.spec.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricEncrypt.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricSignatureVerificationDetached.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Hash/DeriveHashingKey.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Hash/HashString.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Hash/HashingKey.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateKeySystemItemsKey.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateRandomKeySystemRootKey.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateUserInputKeySystemRootKey.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/DeriveKeySystemRootKey.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/RootKey/CreateRootKey.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/RootKey/DeriveRootKey.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateAuthenticatedData.spec.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateAuthenticatedData.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateDecryptedParameters.spec.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateDecryptedParameters.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateEncryptedParameters.spec.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateEncryptedParameters.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateEncryptedProtocolString.spec.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateEncryptedProtocolString.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateSymmetricAdditionalData.spec.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateSymmetricAdditionalData.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateSymmetricPayloadSignatureResult.spec.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateSymmetricPayloadSignatureResult.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GetPayloadAuthenticatedDataDetached.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Utils/CreateConsistentBase64JsonPayload.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Utils/ParseConsistentBase64JsonPayload.ts create mode 100644 packages/encryption/src/Domain/Operator/004/UseCase/Utils/StringToAuthenticatedData.ts create mode 100644 packages/encryption/src/Domain/Operator/004/V004AlgorithmHelpers.ts create mode 100644 packages/encryption/src/Domain/Operator/004/V004AlgorithmTypes.ts delete mode 100644 packages/encryption/src/Domain/Operator/005/Operator005.spec.ts delete mode 100644 packages/encryption/src/Domain/Operator/005/Operator005.ts delete mode 100644 packages/encryption/src/Domain/Operator/Operator.ts create mode 100644 packages/encryption/src/Domain/Operator/OperatorInterface/AsyncOperatorInterface.ts create mode 100644 packages/encryption/src/Domain/Operator/OperatorInterface/OperatorInterface.ts create mode 100644 packages/encryption/src/Domain/Operator/OperatorInterface/SyncOperatorInterface.ts create mode 100644 packages/encryption/src/Domain/Operator/OperatorInterface/TypeCheck.ts create mode 100644 packages/encryption/src/Domain/Operator/Types/AsymmetricDecryptResult.ts create mode 100644 packages/encryption/src/Domain/Operator/Types/AsymmetricSignatureVerificationDetachedResult.ts create mode 100644 packages/encryption/src/Domain/Operator/Types/PublicKeySet.ts create mode 100644 packages/encryption/src/Domain/Operator/Types/Types.ts create mode 100644 packages/encryption/src/Domain/Service/KeySystemKeyManagerInterface.ts create mode 100644 packages/encryption/src/Domain/Types/DecryptedParameters.ts create mode 100644 packages/encryption/src/Domain/Types/EncryptionAdditionalData.ts create mode 100644 packages/encryption/src/Domain/Types/KeySystemItemsKeyAuthenticatedData.ts create mode 100644 packages/encryption/src/Domain/Types/KeySystemRootKeyEncryptedAuthenticatedData.ts create mode 100644 packages/files/src/Domain/Api/DownloadFileParams.ts create mode 100644 packages/files/src/Domain/Api/FileOwnershipType.ts create mode 100644 packages/icons/src/Icons/ic-group.svg create mode 100644 packages/models/src/Domain/Abstract/Contextual/TrustedConflictParams.ts rename packages/models/src/Domain/Abstract/Payload/{Types => Overrides}/TimestampDefaults.ts (100%) create mode 100644 packages/models/src/Domain/Abstract/Payload/Overrides/VaultOverride.ts create mode 100644 packages/models/src/Domain/Local/KeyParams/KeySystemRootKeyParamsInterface.ts create mode 100644 packages/models/src/Domain/Local/KeyParams/KeySystemRootKeyPasswordType.ts create mode 100644 packages/models/src/Domain/Local/RootKey/RootKeyWithKeyPairsInterface.ts create mode 100644 packages/models/src/Domain/Runtime/AsymmetricMessage/AsymmetricMessageDataCommon.ts create mode 100644 packages/models/src/Domain/Runtime/AsymmetricMessage/AsymmetricMessagePayload.ts create mode 100644 packages/models/src/Domain/Runtime/AsymmetricMessage/AsymmetricMessagePayloadType.ts create mode 100644 packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSenderKeypairChanged.ts create mode 100644 packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultInvite.ts create mode 100644 packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultMetadataChanged.ts create mode 100644 packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultRootKeyChanged.ts create mode 100644 packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageTrustedContactShare.ts rename packages/models/src/Domain/Runtime/Collection/Item/{TagItemsIndex.spec.ts => ItemCounter.spec.ts} (94%) rename packages/models/src/Domain/Runtime/Collection/Item/{TagItemsIndex.ts => ItemCounter.ts} (61%) create mode 100644 packages/models/src/Domain/Runtime/Display/Validator/CollectionCriteriaValidator.ts create mode 100644 packages/models/src/Domain/Runtime/Display/Validator/CriteriaValidatorInterface.ts create mode 100644 packages/models/src/Domain/Runtime/Display/Validator/CustomFilterCriteriaValidator.ts create mode 100644 packages/models/src/Domain/Runtime/Display/Validator/ExcludeVaultsCriteriaValidator.ts create mode 100644 packages/models/src/Domain/Runtime/Display/Validator/ExclusiveVaultCriteriaValidator.ts create mode 100644 packages/models/src/Domain/Runtime/Display/Validator/HiddenContentCriteriaValidator.ts create mode 100644 packages/models/src/Domain/Runtime/Display/VaultDisplayOptions.ts create mode 100644 packages/models/src/Domain/Runtime/Display/VaultDisplayOptionsTypes.ts create mode 100644 packages/models/src/Domain/Runtime/Encryption/ContentTypeUsesKeySystemRootKeyEncryption.ts create mode 100644 packages/models/src/Domain/Runtime/Encryption/ContentTypeUsesRootKeyEncryption.ts create mode 100644 packages/models/src/Domain/Runtime/Encryption/ContentTypesUsingRootKeyEncryption.ts create mode 100644 packages/models/src/Domain/Runtime/Encryption/PersistentSignatureData.ts create mode 100644 packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyContent.ts create mode 100644 packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyInterface.ts create mode 100644 packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyMutatorInterface.ts create mode 100644 packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemIdentifier.ts create mode 100644 packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKey.ts create mode 100644 packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyContent.ts create mode 100644 packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyInterface.ts create mode 100644 packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyMutator.ts create mode 100644 packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyStorageMode.ts create mode 100644 packages/models/src/Domain/Syncable/TrustedContact/PublicKeySet/ContactPublicKeySet.ts create mode 100644 packages/models/src/Domain/Syncable/TrustedContact/PublicKeySet/ContactPublicKeySetInterface.ts create mode 100644 packages/models/src/Domain/Syncable/TrustedContact/PublicKeySet/ContactPublicKeySetJsonInterface.ts create mode 100644 packages/models/src/Domain/Syncable/TrustedContact/PublicKeySet/FindPublicKeySetResult.ts create mode 100644 packages/models/src/Domain/Syncable/TrustedContact/TrustedContact.ts create mode 100644 packages/models/src/Domain/Syncable/TrustedContact/TrustedContactContent.ts create mode 100644 packages/models/src/Domain/Syncable/TrustedContact/TrustedContactInterface.ts create mode 100644 packages/models/src/Domain/Syncable/TrustedContact/TrustedContactMutator.ts create mode 100644 packages/models/src/Domain/Syncable/VaultListing/VaultListing.ts create mode 100644 packages/models/src/Domain/Syncable/VaultListing/VaultListingContent.ts create mode 100644 packages/models/src/Domain/Syncable/VaultListing/VaultListingInterface.ts create mode 100644 packages/models/src/Domain/Syncable/VaultListing/VaultListingMutator.ts create mode 100644 packages/models/src/Domain/Syncable/VaultListing/VaultListingSharingInfo.ts create mode 100644 packages/responses/src/Domain/AsymmetricMessage/AsymmetricMessageServerHash.ts create mode 100644 packages/responses/src/Domain/Error/ClientDisplayableError.ts delete mode 100644 packages/responses/src/Domain/Error/ClientError.ts create mode 100644 packages/responses/src/Domain/Files/MoveFileResponse.ts create mode 100644 packages/responses/src/Domain/Files/ValetTokenOperation.ts create mode 100644 packages/responses/src/Domain/SharedVaults/SharedVaultInviteServerHash.ts create mode 100644 packages/responses/src/Domain/SharedVaults/SharedVaultPermission.ts create mode 100644 packages/responses/src/Domain/SharedVaults/SharedVaultServerHash.ts create mode 100644 packages/responses/src/Domain/SharedVaults/SharedVaultUserServerHash.ts create mode 100644 packages/responses/src/Domain/UserEvent/UserEventPayload.ts create mode 100644 packages/responses/src/Domain/UserEvent/UserEventServerHash.ts create mode 100644 packages/responses/src/Domain/UserEvent/UserEventType.ts create mode 100644 packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.spec.ts create mode 100644 packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.ts create mode 100644 packages/services/src/Domain/AsymmetricMessage/UseCase/GetAsymmetricMessageTrustedPayload.ts create mode 100644 packages/services/src/Domain/AsymmetricMessage/UseCase/GetAsymmetricMessageUntrustedPayload.ts create mode 100644 packages/services/src/Domain/AsymmetricMessage/UseCase/GetInboundAsymmetricMessages.ts create mode 100644 packages/services/src/Domain/AsymmetricMessage/UseCase/GetOutboundAsymmetricMessages.ts create mode 100644 packages/services/src/Domain/AsymmetricMessage/UseCase/HandleTrustedSharedVaultInviteMessage.spec.ts create mode 100644 packages/services/src/Domain/AsymmetricMessage/UseCase/HandleTrustedSharedVaultInviteMessage.ts create mode 100644 packages/services/src/Domain/AsymmetricMessage/UseCase/HandleTrustedSharedVaultRootKeyChangedMessage.ts create mode 100644 packages/services/src/Domain/AsymmetricMessage/UseCase/SendAsymmetricMessageUseCase.ts create mode 100644 packages/services/src/Domain/AsymmetricMessage/UseCase/SendOwnContactChangeMessage.ts create mode 100644 packages/services/src/Domain/Challenge/Types/ChallengeObserver.ts create mode 100644 packages/services/src/Domain/Challenge/Types/ChallengeValueCallback.ts create mode 100644 packages/services/src/Domain/Contacts/CollaborationID.ts create mode 100644 packages/services/src/Domain/Contacts/ContactService.ts create mode 100644 packages/services/src/Domain/Contacts/ContactServiceInterface.ts create mode 100644 packages/services/src/Domain/Contacts/Managers/SelfContactManager.ts create mode 100644 packages/services/src/Domain/Contacts/UnknownContactName.ts create mode 100644 packages/services/src/Domain/Contacts/UseCase/CreateOrEditTrustedContact.ts create mode 100644 packages/services/src/Domain/Contacts/UseCase/FindContactQuery.ts create mode 100644 packages/services/src/Domain/Contacts/UseCase/FindTrustedContact.ts create mode 100644 packages/services/src/Domain/Contacts/UseCase/UpdateTrustedContact.ts create mode 100644 packages/services/src/Domain/Contacts/UseCase/ValidateItemSigner.spec.ts create mode 100644 packages/services/src/Domain/Contacts/UseCase/ValidateItemSigner.ts create mode 100644 packages/services/src/Domain/Contacts/UseCase/ValidateItemSignerResult.ts delete mode 100644 packages/services/src/Domain/Encryption/BackupFileDecryptor.ts create mode 100644 packages/services/src/Domain/Encryption/DecryptBackupFileUseCase.ts create mode 100644 packages/services/src/Domain/InternalFeatures/InternalFeature.ts create mode 100644 packages/services/src/Domain/InternalFeatures/InternalFeatureService.ts create mode 100644 packages/services/src/Domain/InternalFeatures/InternalFeatureServiceInterface.ts delete mode 100644 packages/services/src/Domain/Item/ItemCounterInterface.ts delete mode 100644 packages/services/src/Domain/Item/ItemsClientInterface.ts rename packages/services/src/Domain/Item/{ItemCounter.spec.ts => StaticItemCounter.spec.ts} (86%) rename packages/services/src/Domain/Item/{ItemCounter.ts => StaticItemCounter.ts} (85%) create mode 100644 packages/services/src/Domain/KeySystem/KeySystemKeyManager.ts create mode 100644 packages/services/src/Domain/Mutator/ImportDataUseCase.ts create mode 100644 packages/services/src/Domain/Revision/RevisionPayload.ts create mode 100644 packages/services/src/Domain/Session/SessionEvent.ts create mode 100644 packages/services/src/Domain/Session/UserKeyPairChangedEventData.ts create mode 100644 packages/services/src/Domain/SharedVaults/PendingSharedVaultInviteRecord.ts create mode 100644 packages/services/src/Domain/SharedVaults/SharedVaultService.ts create mode 100644 packages/services/src/Domain/SharedVaults/SharedVaultServiceEvent.ts create mode 100644 packages/services/src/Domain/SharedVaults/SharedVaultServiceInterface.ts create mode 100644 packages/services/src/Domain/SharedVaults/UseCase/AcceptTrustedSharedVaultInvite.ts create mode 100644 packages/services/src/Domain/SharedVaults/UseCase/ConvertToSharedVault.ts create mode 100644 packages/services/src/Domain/SharedVaults/UseCase/CreateSharedVault.ts create mode 100644 packages/services/src/Domain/SharedVaults/UseCase/DeleteExternalSharedVault.ts create mode 100644 packages/services/src/Domain/SharedVaults/UseCase/DeleteSharedVault.ts create mode 100644 packages/services/src/Domain/SharedVaults/UseCase/GetSharedVaultTrustedContacts.ts create mode 100644 packages/services/src/Domain/SharedVaults/UseCase/GetSharedVaultUsers.ts create mode 100644 packages/services/src/Domain/SharedVaults/UseCase/InviteContactToSharedVault.ts create mode 100644 packages/services/src/Domain/SharedVaults/UseCase/LeaveSharedVault.ts create mode 100644 packages/services/src/Domain/SharedVaults/UseCase/NotifySharedVaultUsersOfRootKeyRotation.ts create mode 100644 packages/services/src/Domain/SharedVaults/UseCase/RemoveSharedVaultMember.ts create mode 100644 packages/services/src/Domain/SharedVaults/UseCase/ReuploadSharedVaultInvitesAfterKeyRotation.ts create mode 100644 packages/services/src/Domain/SharedVaults/UseCase/SendSharedVaultInviteUseCase.ts create mode 100644 packages/services/src/Domain/SharedVaults/UseCase/SendSharedVaultMetadataChangedMessageToAll.ts create mode 100644 packages/services/src/Domain/SharedVaults/UseCase/SendSharedVaultRootKeyChangedMessageToAll.ts create mode 100644 packages/services/src/Domain/SharedVaults/UseCase/ShareContactWithAllMembersOfSharedVault.ts create mode 100644 packages/services/src/Domain/SharedVaults/UseCase/UpdateSharedVaultInvite.ts create mode 100644 packages/services/src/Domain/Singleton/SingletonManagerInterface.ts create mode 100644 packages/services/src/Domain/UserEvent/UserEventService.ts create mode 100644 packages/services/src/Domain/UserEvent/UserEventServiceEvent.ts create mode 100644 packages/services/src/Domain/Vaults/ChangeVaultOptionsDTO.ts create mode 100644 packages/services/src/Domain/Vaults/UseCase/ChangeVaultKeyOptions.ts create mode 100644 packages/services/src/Domain/Vaults/UseCase/CreateVault.ts create mode 100644 packages/services/src/Domain/Vaults/UseCase/DeleteVault.ts create mode 100644 packages/services/src/Domain/Vaults/UseCase/GetVault.ts create mode 100644 packages/services/src/Domain/Vaults/UseCase/MoveItemsToVault.ts create mode 100644 packages/services/src/Domain/Vaults/UseCase/RemoveItemFromVault.ts create mode 100644 packages/services/src/Domain/Vaults/UseCase/RotateVaultRootKey.ts create mode 100644 packages/services/src/Domain/Vaults/VaultService.ts create mode 100644 packages/services/src/Domain/Vaults/VaultServiceEvent.ts create mode 100644 packages/services/src/Domain/Vaults/VaultServiceInterface.ts create mode 100644 packages/snjs/lib/IsDev.ts create mode 100644 packages/snjs/lib/Services/Sync/Account/ServerConflictMap.ts delete mode 100644 packages/snjs/lib/Strings/Input.ts create mode 100644 packages/snjs/mocha/TestRegistry/BaseTests.js create mode 100644 packages/snjs/mocha/TestRegistry/MainRegistry.js create mode 100644 packages/snjs/mocha/TestRegistry/VaultTests.js create mode 100644 packages/snjs/mocha/lib/BaseItemCounts.js create mode 100644 packages/snjs/mocha/lib/Collaboration.js create mode 100644 packages/snjs/mocha/lib/Events.js create mode 100644 packages/snjs/mocha/mutator_service.test.js create mode 100644 packages/snjs/mocha/vaults/asymmetric-messages.test.js create mode 100644 packages/snjs/mocha/vaults/conflicts.test.js create mode 100644 packages/snjs/mocha/vaults/contacts.test.js create mode 100644 packages/snjs/mocha/vaults/crypto.test.js create mode 100644 packages/snjs/mocha/vaults/deletion.test.js create mode 100644 packages/snjs/mocha/vaults/files.test.js create mode 100644 packages/snjs/mocha/vaults/invites.test.js create mode 100644 packages/snjs/mocha/vaults/items.test.js create mode 100644 packages/snjs/mocha/vaults/key_rotation.test.js create mode 100644 packages/snjs/mocha/vaults/permissions.test.js create mode 100644 packages/snjs/mocha/vaults/pkc.test.js create mode 100644 packages/snjs/mocha/vaults/shared_vaults.test.js create mode 100644 packages/snjs/mocha/vaults/vaults.test.js create mode 100644 packages/ui-services/src/Abstract/AbstractUIService.ts create mode 100644 packages/ui-services/src/Abstract/AbstractUIServiceInterface.ts create mode 100644 packages/ui-services/src/Vaults/VaultDisplayService.ts create mode 100644 packages/ui-services/src/Vaults/VaultDisplayServiceEvent.ts create mode 100644 packages/ui-services/src/Vaults/VaultDisplayServiceInterface.ts rename packages/{services/src/Domain/Application => ui-services/src/WebApplication}/WebApplicationInterface.ts (78%) create mode 100644 packages/web/src/javascripts/Components/ContentListView/ListItemVaultInfo.tsx create mode 100644 packages/web/src/javascripts/Components/Footer/VaultSelectionButton.tsx create mode 100644 packages/web/src/javascripts/Components/NoteView/CollaborationInfoHUD.tsx create mode 100644 packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Contacts/ContactItem.tsx create mode 100644 packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Contacts/EditContactModal.tsx create mode 100644 packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/ContactInviteModal.tsx create mode 100644 packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/InviteItem.tsx create mode 100644 packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx create mode 100644 packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultItem.tsx create mode 100644 packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/EditVaultModal.tsx create mode 100644 packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/KeyStoragePreference.tsx create mode 100644 packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/PasswordTypePreference.tsx create mode 100644 packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalInvites.tsx create mode 100644 packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalMembers.tsx create mode 100644 packages/web/src/javascripts/Components/VaultSelectionMenu/ManyVaultSelectionMenu.tsx create mode 100644 packages/web/src/javascripts/Components/VaultSelectionMenu/SingleVaultSelectionMenu.tsx create mode 100644 packages/web/src/javascripts/Components/VaultSelectionMenu/VaultSelectionMenu.tsx create mode 100644 packages/web/src/javascripts/Components/Vaults/AddToVaultMenuOption.tsx create mode 100644 packages/web/src/javascripts/Components/Vaults/VaultNameBadge.tsx create mode 100644 packages/web/src/javascripts/Controllers/Navigation/TagsCountsState.ts create mode 100644 packages/web/src/javascripts/Controllers/VaultSelectionMenuController.ts create mode 100644 packages/web/src/javascripts/Hooks/useItem.ts create mode 100644 packages/web/src/javascripts/Utils/Dev/PurchaseMockSubscription.ts diff --git a/.yarn/cache/@standardnotes-common-npm-1.48.3-83aea658e0-191d97879d.zip b/.yarn/cache/@standardnotes-common-npm-1.48.3-83aea658e0-191d97879d.zip new file mode 100644 index 0000000000000000000000000000000000000000..0be3e20871afa9a024beb96dba961296cbcd03cc GIT binary patch literal 36111 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�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

o7u>Q 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%0DimI9DQrnF=&9CmdaiDZ3ZA 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 Z?{})m18} literal 0 HcmV?d00001 diff --git a/.yarn/cache/@typescript-eslint-scope-manager-npm-5.59.9-c9c714cb56-362c22662d.zip b/.yarn/cache/@typescript-eslint-scope-manager-npm-5.59.9-c9c714cb56-362c22662d.zip new file mode 100644 index 0000000000000000000000000000000000000000..8aa15221a7e0608757f8e9a98c0dc4be751cbb3e GIT binary patch literal 324961 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

)Rq&+3R&W7p*#0gBfcDm!C5o%=XLn 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�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ýj>HuAWV8}JU@7Je#z0$?N1<;;<-`pr&R;|0AO`ZAK-2W2V&d9~uT!Ujj;8AKd@py^_LMJ%K^1RGg&8OLcBHt482@?1Rz{L@G3I5J( zo`+`lzy=sTg`s*MVE?kgtP(#->;;w9kW7^mlIeS>TmQq?4MD7VX_Dc2(7CsIw=FHh zs})Lul}q6t5ZK`6eJ4E8~O#yAO|T zShN#7FJcz8hzsd)GG4QQfcHsE&E|$7P_`C!GqoHp2UKo0*s$^nDe#6_tDk3KpD+V8 zt$aoIJ-l2Tp2|FZR|{_w>Me}k>0}q^32Gk4uS7#ScayvZ@nyC!Exw4u>1{WiYGOwD zYJ}%8iV>#7(D0d{(F&e$mU3htND}G)7D0+>YuW|S;#-;rI*g?Iy z2rJG$K)4XAq}aAfzq^Y^wwmEF@^QWKKZuzw7u zcKOvPG5t$fnUL; ziEwLIXyc^ulqekVFUC94al|r$azq+UwfPy;nIyzQ;g@CLw;L2NGsxK3*xa5Ic*#%{ z>QP6~V|GwHY087#Yu7%YM;#J@;RB%oDW@qq<_mao?U1_ex)*{jagr!w6)6b~Mo`FN zo)HkWtcPSQ42nf8(^A1Cn1ps?PT8fh_Zz5k=K;V=4y?NgK`3L6+PJx_vUTnO+~F=m zPgjAEe7=vpL=bC08gR!~RO9?~=*avSa1x$6JIVMQQlOQTH6&Nc`&4H=L98K~i6DhF z4A`;7#jAC3sGfJ+j(w4X1lZKPZ|2jj%aHkwaKY29PldC=**M7ei*J|?k<`Y59I3ih z_AA4n-@S>7TM`z+xlo@p5T6dX=gMYTDc?h=D?)Nb2pnTeILJqV7o^V?%A1!<*YKwX)4Pg z5WW`Y&KvGeN966}N{+v?@cxKiBRlcRa|+%*HK!Wz!SA{O-Zx(n?=&S}96^4Vhe}e# z<@$p7=m9&>zgs)J3%|g0oiFtzn#wTZMf5MC2sskRls7^OS4#E!it_7(Hxc98-m;~* zAJqNrQ!Dd`&kALqdwChRo7@r1O@;E8^ z^bM_s-gw*A%8F_by}e~MO!y#SOIkjnP!pF86ydeqHz7l$L?d6Vb>YBK|F)o0`02dQ2JCZYl7M&J2Z-i0a zf~{}DXqm%37GEQe6l5m5G}%T=$KOrPSCrQBD>M2~SW$WSZgT&t;N=oTP1&_+QP()UJAKEuZzj4!fIZHF>3f?UvN6a>Ti)Kvtn{tiz9yx)3#o+hp zu$%FfX{^Fr6Rh*uttc7EjJeCqgB<)|HTEUj;_5qEX5%`~ka#$3Wc8ZzUH`rida5RR zM-=%T^26QloB?Q$S5pi7Vb9;DM_zKvzr|PNPx2*aj62%MFEU71bj@&06;qzj2t;lH z)6!$8fi^nv-T%7nW3aMdxJR=SIoCc3T33M8Hx)8<5Hd+r-y$zKH3t5OF(Px@x|AGa z*HK8tn29*DUmrtN-wewfR%Tb7o3Jt`OOwv<`B=niRZFdi;opg!a;h~Tt`Xi{496-G z^9wJ`PL}1UDCL8@L6O((=6P+F(!MGFEsODP47ZX~YMc3(%;31e(pT*1zhtt89LbPX`uqe75BRdK7fW!4XC!%pNxoMXpq z7FpasI@LiyT&MP|BdX19d`S0vM-*)rA1`mWt_`&}(BY;E`(!(~k=(-VXiTv_`rrm_ zqj?~b_2>84ag(QQ-z3ibRVektrNdBZy&_tBvqcFB{gK@>&tIMN2QI1F-wu#q{=^+z zhAz`vynBA*xmthaak$!Lc~5cizCfgsnZ&dxN{_!^Uk4PGg&tHzq1Al;qo%P zwhEqocN3SnPsCU^X7aE4@bW!x4sfqiI)&&+;!%-fY%xMciKf@`_*1{)D!(KeBXtRZ zGCM3~96Pvyy{+Ku9rdl$AfQ{8wo3G+U=Z~PbLJ6SEP?RdvSbKc^bJ6q3>Z5+B21bw zJERv>#aXIV6|D+IYPYt{+=M0zwaa;;)G`e6pd&TYghLqT7=dV4FXt=t8iY#axpe08 z2`0cIYC}8~LgfMX2!|pLvly?$!|!9{^sTylvQAjbcLMKxnOIVxU>YNukj}!|rJAs( z8UfRQ)_p|e1zl<(9ER~kcz}3mhb*Q+=%}`mmJ1cn#y8Aqu$IERB|1SxZnE6UG$*yI~hWeK;eHhV6uCgZz^fsnGW}CGq&<8YgdR|ZnfU9L(OyJ z!t!HDH*)4MR1k|OlgYY7J~$6EN4hSGIl3YVmkGnX8-+ORaXsyt8ujM;ElnB~e-*tE zk``%8(a4+WEc@zlOImOX+o8YblSImMaHdL>O7lA&qcuYSd_4Y!3L&P2wIFrrSfTKP znUT9EDBZ5rFb!Ft__T8t)MkK0{wgphErHI!T861ydBUiPXev?662)2cGIh&xi)|)W zpOSwjgJ&AdB`Qo~+3oVZ%#R(nC#ho;2d$`nMeipg^qGt-ON z?nOa~C=yGv@a^FDjF$YyZLOOa(ECaz0%zrLotmm%vRx;QR^sCSfa_}WL%xPeB)t>o_T-rg-^lk?DSe}+;? z6ABjwMpGSZulP@$ZGDMnkJtPEhh#nHG~(;$LCGD79KG#w%Y+GzL|L%=*s910eBgb3~8MXN$_bOYTVl4A<>RhSL`9G{2((WkUtrcTff+t7S+=f zJB+`#TP@@JK76yxfGANxe2h^Y!kyG0ayn2h*+nEc<;NR^uz!E5@zZNSUz|=o;$I_j za&ntm%xJ?%j&`%WRx@;VZAYvTMII~@=eih)<%F4lbOxzaR~lqK_(21nCW3w7!WW0N zz(jg9p)v}y6&*%>8dC}%H(J9S7Q-yc1QH@Pa|rI(w=EDlST6~ReJ2wb<7MPnu zhq2f-LR~NGYN)Gd+(F)5!)}4T=Ak`yXMrq{=x~}0e+1|N4RHd|MDY6`9_uZr&PdQ0 z2{|{KBvv@<(MNCU-yrZnLaM|$3<=$N<#v$U^X(BYPCSP~Mw6^hM&4CU)l*M?Rj%8&1vN3s5p(#!Rz7$Ec z&LH#gdWcRwG`{GM%&85IO-cByi#g_#**JJoE>4Wf_YKOdnpf?O(EJJ{<<{^AJ4r#_ zHTxxivn>aYrdWy;rd2@K6Z~HHLYq>D0sAHUj8?Zv1|JG#l8!Tni;{r{A_OW_UchrL zu}#+(Ve<=X6@V5$Wv4e83}=%XFejJolPCoQZ;2po()=(&*(ktR;?R)u$Yz15_^uY` zK4aew1~<4|enK%+;|w?woRr2+2;6I55%M~Kbtg6!3dp1tXzLg08x?5gsyX!P9^~Jg z0ltv!aqd~~H3iaJO2oXH@v_c~n#l`9LdVE6{>WwuYX!|F08+m(;qHFjO2Aw3qTCHm z4Kv)Ek#_g7!2;hLll2QcgR8Ud0i5?OjP8R#BwulLSV4K3+!x=mruEq7zJoOx!GPHG zv|*NI395l}hKk=Xy!*=roEY11mP-`ar4{gYoPfV~aVT{+krGrvNUcWpo$9myGFZaA zBw|Jy9f%t`1@gV9dgOowwyDo<;5YM({F!S>>X-3;i@Pl)^q9$V>JQ7h&#U{=y;VOd z-9S`XMhf`G4^5z#8LwVpLf{%KkH*QHz%teeAs)SEHyr-U6M=?*PHkKAYmL$J4oeu@ zdfU`c%8>j|B&mGx0s2E@K^5D@pk*7BBEBFa+lLuluOf&OVbZZ6%W+>wUJElNyO_rW z1>a%RV`=AzTz5-VeRD(>jbg#@4Rz7EKiW;oGxhK}S(_e>`gviDSuioG>6X|AOMq@( z;1V;$x8CThg=m^cUYm{v5jLvb4>sH?u2MG>rA12j221y}Ma~5z5c`$(qpVlykH|eAe5fnN#}`eA1ax#c9G|0YYpE@cuB86U$i?-a zU9V?9ogxGEHYSccHWK30g&VlFBvC(td<`g8s?!EU3kKdORA z>;saf@hT`FrlqbRI^nspNoB}0^FLcTql3XwB$_^8kFB+}m$h%}74%l@X!c&ak79MI--Vjt1zkKH4f zvZN3MwyMQt=e09oxN48K>zitx76G>)Md~cRs=id*qs4zXJ+Y7-E!P2pJOxL~EH+rM z1gEYJnZ0XY0fv+!HM%~ZbYn(0gNKe_V8O+;7@N}lZk6$-qtB{aTSzNJMRtxfAgZyE zZ~t;+wUtf@or$icZgAH-PK%cxEzfT-nQp>F$sisWZfA$gM&rUNs$h6{*DW@D*O2DC zbv{!RlWoxSb6I_vJiF$ME)xZ;p;BLM?A9QpC%%axXwm|E92JQVFx@$B7WP1B2R)2K zSw@;G`k=8<2#XGS>k*{>3KvDj9Y4d0dbV(L^!z$IJv|)y*84NvxrxqORF6@jwa31# z!cr(A_Q+7V1>3g?+hM$;KtaOfj44qfJ98ydn}R?a0%SjhkVc@XJDA%`&sYEn9>r&< zsj&g`>zbE_jH^BXqpB~YQG?wT;}s*LgH@WkIaespZM$>1*XKbLwWvLnn?_8i4}kQG zy~9Nu0_=}4r>Tm0CQjuwUPiNCD`UpXRKk z9M~kNNd)69E}axMk@C|DI9L0uewZJg*=1S*vrw*V32v{dKBk}*NUhGV>h`=+*R;8O_b$dINZ_*sDyV1zia}&F1HyBA2_i~rBQ^A&&2?awckHzyN*Sw!s}Sarn)rM5Z@lf zeD<uuTaM{HmCo%^R9hIFwQKngWo(3?o5CW`|lgq!r*9HYx9u z2qiW-gcMaH31e&o>SUi`5ow))X7;3Lw`Sb<#C-9joX7J^U~doVo5m}Z6=A6{CFM64 zuMq0?YEzQSczbFfJd`c$Y{qRpsr?D!Nn53hIe-QZp@UgDNXF+1hEtdkTkn3 zXyV0K0Y_|gW_On<>C(J{g2uzVR0(1M565s;T;47u(1yaAnqUl;8V=}l2_bN zmiXeNt0Pe+Ic$w_cadqb)*8ShiD+_~K{b`aCrFff*REuWIEKo+W7wdD{w#bVFK!OZ zMpe>Ex$6)4#z{Z|QLRbtEvV7p>YB|#)Utmg{ObIkRM=~#<0bJBCh5YJ&O zD^>)4sBpq z9#)*}#>cENwRi0dBV6`%ZXT|Fo<@af3q5jnmJ9`nx=4VJFyA!^3RhPJGSX9(ei<51 z!&nf}Tx+xpCcZCS%^H6@?YB(ddM^tI7)~n)OWLE=P<+CJ0yzO&^lE zF-X(6U&7XZ8`U^nN^w8cUAt=G9vbk{tMz^Os`Gab9KgL@kD`9kyQ*$ojY*h{Kb<#n zeJ$PD?Qa#-c}b*fK&5Mr+-Dp2U@!#N8`q%-g;sXb_ETeu zzeVG{Gq$5W_(5L6l}&nb*mAZ7n1q3|_Yf~O8F+}5_rj_ZaR&!KiYB4el#yH5RQX!? zIM~bD3O$)-th+lr>{uu#q3@JmfLQ9PN2S-hO>!1@R(+a#t&($Hif^z zrp|fvhs99-T+`cjpmA%DS|?neU^9>dW&ndO^Kxh{fT(T6x0JV&1Cve9T%F^t-fbr( zYySA8@dDEeA}(l=8L0FaUm(e_M2QQ%XNnE1d)!LSBz&M}zC0#l((Kwp>RG|{O2*uc zY_7q{;pN4=?23XTU1jEhx1?Qi%k|ddO2K?Y(rBl%wh2@!Zk~KG4K)iDNkW&r_2v;R z1y8%_wTC=(tFE}-dCaAt1{;<2QCh@|FMEBgJ&HD{hN3*b6zV)6QBN;{7EK~yh@YaxB4(bufbJ*P)?A>ybgvj29bgxk3lAm=jPa99L|!Yt*1 z^m3bWwHHN~X*(Jk;G$pFaj*OJr`6Bo*kHcwA(eTsVDL}UaYv3(M4-O&b!{-oV*1wmUVY*G(Se=-ne(Qe^_FZRIvA` z42FPp@-GAfa;vlBr}>Iby>ea{dPNn5|JWLbmfVJB&qL*G?ELNgMW=tgoa!75?Y7Jh zd*ypghtt6cG?`VcCS}ees^6;>e}DIwG@5nu{5EI0u>qhIyeE%MrvWLVL?z!N!M~Dl zFc_{j6lUDvRpo98P$#l3{;v^!auxrE+5os5M#;mqAD8h;&Bq#Q=hK89i%XnxTXWgN z8(?U9kv5E8sJx5oBsHq)_8i=<2Y8vCU5DQ9R_&Za8&Pc65vT}G53#P{FSn(%7ru9~ zu$^VE(s#VatLQ|xK&&rOcS%MU%1#)sB{pznI7e?T@Fe;dE7A{Wz`GHP+#YPnm-55S zQ+rDFyC=1l^$Xj)q4~U;y}4ic4tS9lNrw89E@U216;SQcB`y4|#U-AV`lasKI@W3} z(BRe18~Fo^`yprk2BH!v{(9c#lVporUgnu);u>lLhY+N6W+j=nPQk}~*JWBj7y~Z% zpr)ndC*BVT@b~~8h_o~w5uO@|5GU~MCPmOeg8;P!=LM3rS~xV*3tCbkTrB#I=_2!U z;$9)=`6DsPy`Z*SR%3G*s8I3pC;}n-6BtCYqnI$Q{NYn!o>P2cNtw&hx2XY#Myd27 zbphkmf=^QBbd@ybdL))l5=)K<<~gE@$lS)ld>|{YTZOMCaL=!WC$8x*;BL&AG zxR9Ba0&^c1_x(zM;FyK(RYr&7zGSw(Uj&T7QIa0@n;8pd)?p{I;zUGt(EVCEEoL$0 zC#Qvce&HcJG%xEj_zFAi93|3=^f>u?Uerd4g&%gUTX<7~`JP6Fo%&SBA9SVtUJSYR zNBMiGrFECY?b1YV#=KJ=w6v+bDGm^LaAs$8?G4lzob#u}&)cPI8<1PqD^i^@n+@;T zyNV(Fo9bFR<2a+X(Bqh$L>ICUlU9o>Dz;uy&!Y{XIAV{P;ceCFOOx}A15YIm;0N>W z34E68Wi=bLjg$6y}nrWI|rPi(Mk7)1n#k32NzFUYjtFdIL#ykcN9P`&u|C*Z9z* z*6s8c?Z};xm%(Q#*gEfOe-G5xE$OyVRM%kv++}Ypec%OW>6>rlQ{RhXks81`Irf?G zjdZxbF_qLlsKu}wbn)>qJH4C~%U|$A+n1;Be@6k-2rzfq{G*2c^9cTTwey2Szldl6ww1v|S}~c)7UGyR6O>RMNm{kA05G1bK=Tdi zOx=G5l^al^u*o!+KU%&yyZB+j@aOSUUbhsMFL-+WR=QNWDJ^^aJ((J})NyH0szG4Y zLdqiBTk2<5=BIG(g=9v38+N>>mW1GY#3VK|89seMb~wS|GK}DW^OX+3{bz8pWs{p- zzMfKka`EK=_!p@{9anOpf1kI~w3UCXAcThu! zS+(kksf<`iRTVv|bbY+lH(k8d=YV-aP|hnfs!1{vi!bGSZ3i!FT4(+)O-oFlwZpa+ zUjnJd<@NXL&uPF0ND|?B`Gset|uF}2TbJ$B> z1qrbi;6qNE1hR!=P9NPDuyF!^%z%h73^SIho&QK_)q+}Vr}u25GMM*?NShiKbmpA9 zH`HQ}+B@K8YAI591JZ-vwN3G=n3;xifXiz~{rWz)5&1zyn-;hF^p)_kv!YD(yAy2c zE_aDD%X7Cs*dHwCh6527kk{4pHBKy0w3)$APRU%)TeM4r9@haX9BNsShW z%Rjc(CglfnL?vDBP;GsmF9w#qk{oG1o_-L+!SQ{bgA;`u{vgn-8x(KKSvKP{^et7U zAwXtuEg~tTs~W|$6Ae38&ww0iWI@uk^9}7@T){9f_4z>cM6M>21XUN*Q8Q7deFU~y zSe~nc2>7)IHA_owo0eX%y0#knY@fJRrNr^jijH8ENrS?Of>Dv(b;0N(&rHv7 zRye0tmh{3P8h1@bMdqsY<`}P8r#Vwr*AjZvChcfmod?b2wgc-KIHyVJ!bV1S2<)th zUei)BfA5oL$_V=H!yvq=a_K#5sqRD93#5T;Q(3H?uo}pxh#Zp;@uVJ8;u2~!LgUx1 zo&SbToNHxcIHT{!e~NAU)dbkOK5?SQ*WD5c-=FtjDDmNh7|?~bhTSR3!@a4tzr8+| zoUhpb)!mZw`?yie#Jw2Wvj&pzCw}2(A}g1!VZ_~+{J@WY?}kODvO1|;ZCWn%SiwT= zcdJscoUTSCW5vwLWl%-ZQ0LOO?ua*qNVHzXXunh-olgS3cY;c>SUWoA^qv}L`9jt? zc}v_QZUQvw9m|6-+%|6O2;m}LV{oW}$i#t8sNCuNoqF$?HBD(wpPL*ogSM&o`1=j= zJ7DlVpDq5&U{iEvN{E4ublboM4&4z6lR1o%fOb5@ifPC)JvqU^A+|W7VHWJ!N>wh2 z_Py-%)q>5`auf!Lp-HfA0tb-Xo~8^ALL(0~DTW%ve7&PGWL7YP z!ju9avrW1KTd)E_cZgA_S{Ait-~$PVY_DHT6ljNXtcSRpM|ey=H?k<~FHNXr=paBN z40;3R_(qpnc1UwoQ9HyV3}BRW&?YYyIudkUREK~EP5EaDuRgudvQY6-$TM?0u>PD;M`2&L9ZlVC5UGZul6#;Mh?Jm z`o8%m&UPp{&WZloZC_*Zqdng$r~ zcWnr9SrQS7M|rAxm3OG@wfvetgU=`-9NfWM=H+b?l2yU`8EP2VoC|Ck9tF&yZ0cYf zAd1eAEI>&3yBY z6}%&xbo7dsW|w#vOYM`k^rB?R*C~*Fu`sGf0Bcs+o(cNYRf744PzOx!w?6GYlZ zSYSTW(v5U(=6r|lCye?BbbnxwR=MuuY<>@#}nR^;7G@Dj(u) znb_d9BO}KstOn{Q*_W==Z_j{ry{kl)MpOp3Qfbd&uLH!dP;u$0bofH^y7DLN_>nHN4tB3}j?xiB zNH}vK+t=%t*G~u=O>Mg|{7rbkz4|gJMQGbw3p2{$s$kGj_4F!s1~Nz2>nbg1nf2SU zz%uKG$Y)D;HcrE`S~WvfEr2yNa0%_5U$Lg)z@2xzM!@dJBt zRE15TuSeQz&-N=fh)$881xeZ6-Pl_7q>L8t9Et@h8xxGO6~*Ww*jXTU(tb8k1ynu)9!ndbUkJlI{V_%$4>$ z%~>4tjAfH@&De--pao%zb)NduWAB`00;zAID)F!Tky0~v6BBiS*t3-uWm5<65nFFp zB4#78WL^i`J<^`I*5ac@y5z;W_dDdxep=^}wUQxfAH2`yWiV~aXZ678zSohi_U6)H zbLj$mj4ZOv_T-CJ`@>+d6B>9vyU5#44jyKN4WE1NI%>>i&S`|NM0UCN3rYCo?%JE8 z=G*jz&x&r&QIk-?A5)Wd*ODuNGuNG+H#+>U0Ey+GH(S;c)jlG;kWGNGQylpq&NReT zBGpuli@XyGmyR^!X2Own)xUDnPdX@+OiR`TvSg3I!i3PVs(2VNRW)q439#p9(E8O# zj&^a5Zs15A$~Fd_=_S8asyI+GW8p`Yb!Hc8K~u9d7MzWhOM8>dHZ_Gz7HI+FE6L`p zU?@)9e2t!mv5J91ZS9K2Ux=vj*}E#^3IvJfIT4j$pe`)61p z+LXh~yt}F@h+A>7$!1w18TdU+;D?l8nTGR!D8i%DwO{kXUQMj$b&QtYf-ep5T4nR9 z>MrW`z+~;9DQ#Xr>8pUN$lt+DC~w4?5d>_0ODUZ>&!0UCWGn3sJ^a3lZ6MDb%>3h) zc$YTUMk>S3=J1H!W>`)y+hS?Zt47>F9NNF3dLPE9fDiWR_NU{wGgnd{O_DlK;W`}e zn!{;@^l(S!>4{sFhSG7;v@mRv2%kAS<^(~qL}zNCg638QH*U+Zt9No?-get!jFHSt zt)+5ygBtXT-9;IgGmSOR!^8gIG}DG{i8j)3)iuqf2i)=+kp*u%Q=6Nzf?VubNRw9~ z?wUn)-d7l|cj`;|dsU17rcRmKEk(OcaXP*k{{Z6#I}`T&l*PU04=vJa`GdSYjsV8Q z&H2&b`njZ!%3bJ8XH1*o1I3NiBd$E1+hUwm*0p0npNK;e4@InfbIt5?PLX-B}~uU|{XT>QY!c9S{mU|5oMws0?I zM~G_Cv)-CjHEWC!`0|275ySx4;fXM$Q{YlpS`q!`|nWDU#NlsQheSd^lBH`|%`jn!E@|5sLb`9U-J*CmXOoWS4x079Ab7a$H0V>vwLcTUv!kQzR%Hc^458L?XFzNLA9Yo8x$6d&FYxr)E&MIx4UibhuEVlfo=| zK`0-1?5>c73~*kQfQVd0Jpsw3%n?7MuhqL6j3VUF7fz6L+gaOr@xsVS@vyDr zCQNc@myV(5<94lpG=vk>h2{S z0h_q-Ne~KZO2Z|nN|hY3a8HM@QE~>;OyT4NNDZs4WW`Ukl#I&r7)nRPx0$yv#rneW zgV0G~@|a~ZQ>wQ=_F8&Emot%g=dtGLY#_K>XcSMMe9D}>T&b(3@H1 zO=GgAOZ`a6)N3Fx^yfpB!u*Dv=qR9z(fJZ$$2C+u}&H5!FE{UbZE zlAJ70T@&b>*g?uArm=)U)KNDnS$8r&fmf-qBWQInKByO=S*fNo3*8*p`6|)--1bu` zZay`6)qPnVQTM}2*)$$D6CW02jqwR$vkO*VUf!wl>l|e(io{boF0w-yWfnN@FLDlWttv!Z_oSEsZ1C?T*&j!{q-(qne8ZQS9! zQoiUQ>>V3hMvF3h$e!!Ap1;myt8dSIYvHz#$>OT~IoOEN>S1t-lNFGCC2~b1#38bW zJ>CA)svHzG0%93i3pod@rk7g)=~P)M@`5)uXfOvziL}xvW!*(3_iqrP&#;y zIid5qevuAB8nZMZbzQ^a6`V=JqN&Se)sUI1A>`{GE!2IjSA1=$+j$D?ie^tfm_{0| z(}HTOXdfay6h_WNxluc6L5XF1E%odm&$>mFG&~=n%8*_t!`S*KkQ3Wn~mAMDA=`nOVE!)9Px#%XyVX`;T zxW>|Lq-5jKHRAwYx*cW{?H`P6YK9lXb=9th zRg{+HYz9rgn%u>RHyq7>cJ-$cNl6f|nCL7~>gyR`=vP6ZCApm5Z;&;}Qu2qCYcNaV zdzu_5LMPV5l;^fsIRD>IIIF=K{QjhsDI=#(ZPrO*W0~ z%EM{49WzeT+C6=39ozOy&`+DO>4)*6_FxA5pSf!grFqqmF6Q^{-9*&swoxU4%~Y}t zecL!Q0M9ud%^7qb(ktf@!aW>GM3?~A@tc7Qi|pQ@3t_gV5L2cFOH7Ho2RqNPrH~Gy zM^`PYDBIvBsu5AoJILn2je;(S5C7!FP0yFwek<_pUvz2E$I0hyq2N{LDjH2ohqeJW zSgbxVvkVU>R-RV)t?+5l)oW~?2$(iP#U%v=w6ZP3 zi@_VIr^yFBaII7u*3%V`Go>?9)(qb*Ydz~bweJx#UO86Hd7-U8fM>7xfjt$fCy*vg ze7)biPY~dHMj6Q>{(3`>gRgEF187n=0Q&2HKCJzN*$22JofG6`0bI?IokyzlXOJ*l zzK&~yfUvMqWbdybiI*Wm^YqrCU#_<<%q}3d9`3HTuBtz^J?x8ssOYHX>@fh3v)`Q3 zg#b{WtX4r_# zBF3)X*oX4$h5;WQjw%f<21m6gw9io^2Bd*&7lt7~K$$3W zKqE+Cd4{n zDDUgidfD6w43)eDh$1uXc*<2xjA4$fDmAOnUrjV%+4de10Owfu_i4<3NcQ~06x__# z(Ft&jQ3=|Ac*ura0UWFAGh_m4w*siAkXoXW2PvVAwM5AAQfb8N4I%$@kS$!$Ft5Bx zs(QR}d^`Qa1WF;;o?_YzY)vS(U+M`jnHIcxRCusnZ@AwM{E_D;^hE1UK)0WoQSbW~ zPiFJ>07wj7KO^J~zkw_$p*pyI&+o!<@O$*gb7UIfhn`%f%m`t%)l%R3KhdyTqU-qt z+yn5F#pieJYL>63GA$Ru%huuKuawP2+eyguL;j@2* zM-upD032j37S8AJdztZ$y8D>0ERb~gWHVJ~&m`X}8~rsoR4hPs$z|k|Tso6+95=Sg zwLg}MgL-g8+zvbn*l=9pJf$Db{j4h)fBoQZUy9!0?y_P%ekOs%^s@60gp)Oq82|DP zdy$(g0iVYsMK>3EW-BFU^C4F35D_V!&A$%KkRjxmhJH_BdQqmRsFk+37qg&quUqFM zDbeliLP(`qoN|?JwcFX4bA@_Ro(DprzEZwP7k~Y5)p3*E=o)+f7g1I));5lNKw0}A zQ%4nD&7BO*{`=|3IaWXxz{w2;(AWQ@j`k;6U`|VAU}aX&-iwNw2V_M`D}HjDvnqqb z=VW%y_%Ou%mwSpq6K^hbd0*Cro%A%IFbSc!qO=39uZ~Np4x<|fp|@MK9u$KDY|wY2 zsLEM)TQgqaCj8U~7Bj_dtqHQnhs3hYVKIDBQp^V*JKPZE6K>L=yiAn` zqP^~alSj9KJH*M?JZwAOHzsZkL`y*{WH0_|!Yl6uBMJfdozwyB(f{0sLrGD@#@Slh z*xKNK5lDFfNF!~%)5w@e6ps2QmBI!@L4gIcI>ghoI#?YvRH!I_a7!qEBcj{18|Xsh z?K=2FhJFc5r#93hN*al-IV!oJjsk6B5oOpju1eGUKZAc zH2ml`D@;2~JL<#bPC-B&6n0f@so%H{$C9!a(TN1QqQZaohJ-o>8uJCiC57Y>{m-_2@4@Ln};bzX){{{#47>wHnZ zKwhiDXg7pDu^@VewK&RTL{;);_WGfTZ3%E=_(^?3WzZ6`3V;EM?rbn=f=u>j6Vtfxh+XiP27+x z^4|f|0Md@e#J5MzydWzlt}&mSyKFiV61&`GF!ivO3t(wE_)Cq5tA8h=AEmG6{YgX@ z@e28GBDzUe<4r4@IRS>9gEeSO663Uk-_3&v^Rlc2$y@Y4x#%|ZAAH71sGl zKsI6cpKLNg1IQ*r2NdS?D{dQ}Wx199&{mZed-VoUtZ2KC?Quc(%EWG7G2@2ABAmkR zd@6J%XNk`f^qo{F;U06jBi%R1XX+kgDS%XBxb;UWQL3c6TQWStx zB4R+d^%sKTJcZ@zAG#GA0PO!GKOZHlyq;u}#v&)0fwSEm%G;Z`kb#$Is<+kZ{UST+UuBFhZWxpk2 zPb0Uqw+x#xA)4nV8C{1WMw<6M*;$Y5P>~F;bM-olf;_E@G`)q#zqCHx1JCH~sNJST z(|Ozd*v|>+5Lrs3;nz9q>xDyT*N@zIB;lTW_v~8hKyv>8vjR;syVTku;NMP(z%G!f z>~4Jh3%zOXQl#k<04+5jOaD!B|G#EwK;Q6h$_mc%|C*ow5z2)iM0!!1hcMOu3}saC zSloLE#7Dl%(=;)FKb113+dwuu_j$mHeFa>%+u3ZWqQ97_cP@%WpM1`D03kl=8Wc zb_-#Oq-0?`WbhbZX<2|TfNpP^K>BHIjCO4|qeBcj)`lTx!GW9ZKLArA&7z#_UL=(E}c+Sch zVd@)}CQs%7gS@JKfAD4TOwg>}O?xGpekX_ydoyfR zQ=n4|W`cZ!tPdWn`(pf)`NV_53A#-LKOW zHJ|}fY*au%QUGXeY>kX{t^X8Iab)=B1PI4}bV78-j#lP2PJq71*3OvD`X6Ip1|xGv zCx(Av=ly}}{|HlDXaIyMI8XrQH+@fjQC)UP8#S!;UHMZOL)xwl8JqM7p@AvMrKwSZ zZKUWUX74+;CZ(8?`BoItml&b9pgX`idC8fP==i5?|aH8pz0 zh0PgZa74z$9vnE$#PLK2Gqssm+NcpLlnM7Qqg+za9!x@620z%=+KFa!)on^AEs{Kh zD?2n8s@b#nP1uj?)EmBF)3ue&rNJ>ihEzy?>r-jEZh_@k{TWG<%)l3pMVZWL7I)W& zl7>VZ=C`B4Xin4IR2!^Lko67HVtUQUcJ?YESua!OIU1T0TIx zD2{E#kxn5mI6OET|6RCK6k!WCt0t&9qpnr=Xg!?@cj~y`Ycv6b$8DUC8`fj2G6c89 z&PxKzeWZZt=alnpeS8DQZtiStPWUFs%5} z_J?UoF6(3*2UO5wUP+I2B<0^NV=@yX!eZmUgOMLqN(Wgy3oM4P*|I7lu(Bg@wP+ef zVyJ)=eXUg(;LUyAs*sAb~~wVKM9JGPOVRMS#Yr%xHs7zpp~O8@qt}WkGFi@b7hF@ml3sYEWe3E zFU0SnV0sTiDM!x0CnLky%TMeg7wgp+mhi{N2_zQc-%UJ??otG@03ki$6a`y#g_@gC79=Nk5RiQ^ z(pI{>weeCW2LIxqCW;ej_%-eRasW?lK7o#Vq@B0 z4`^2`7MY30AsK|!gv?-!dA1IiMs0!m1!oP|mOcoFOxIANc6HXnT2xW8=@NO&X%YOC zqnBTal)`#V7RcJw0;(Is>01MCD@5=`?rXaOP?eo)FF#A}AU4{UcgnNOCaHU0^b8$4 z(x4v&96j3TDx})a_$3W~Qn@o0is~5H3RPq!GFVCu(-oFB0&sA>swuIS0=Gyp0*<)N zhpuKw*h(unk-&Z4Fj73Nwu2_R52rr~Nxu4*`KUrrtMY_i?Z2{8TZ3no>e$9-GR#3L z^gh4CfQ0nv%^ww7`d2&LS0a+i_99m@;~Xm%9l1SfX?Oe2-zn(OAh-~1E5v=*sXZ9> z@yTwAnN$`eiZvCY;;|9eN|JEz1J{a71nK=%MPZ)1z1qy?*9pxw}e9*=<&Zz{vTskfQl#*3!w|NN3NGkL4(#TpW^EA2r;f z;BNdgPQ^wPGe(Bt*y30stXWYJZ=c14RpQa3lwQt+4r4+?)0a>XQiosaggS1?Qr^^V zqIF0K4tum{egWo%zkrC4NkFad=7G|^#;4&AyiOmo4Q0#Rp%0tI4 z+gF%2B&@4L%71g-?-+yPc6E2Ae?Bd2+&(Yy(@!%LkoSL@jtA}n!z1m=3UrvQH){y3 zHr+2SGX@>;2b1M`)@vQD4Yu6pTq(DV3vrVe(qb@(14qZT(h`pU81l^X4bRY~?7)yT zJ2Cry20T$_B1XUbJlZ%7#E`p@{=zg|JA%WPA99P*8{!%VsO^U{%aW<9UiPp*nql zX{LTUs_y5X>UMl>)AH-DVavqO z{qb=o<MTpKbHgQ0QTbwhjQoneF z@Gy|zb+CYEWq`owJ;KNUC>jR_J>osW3?tDex}&4t#0ak4{jy)1YV^4x(Ot;9PPdQL z?c1Y6;$GKGKtkL7ws-Gut)p;J{M#3@<3*u&Q=X=bZ3$;}!bvdjwwK^n&( z9q{kAMoL+@R-}Zz=Zu zVUN@GN&|JpNC9YL^3WrY>|KG5AX|S!L5}I< zq0~|v=SGhUBpf%W=HLor{Q!y(s%v@F>TPk3{TU+?m7;P6!y><>TX$f`Cp-H1Lq6g> zQxJH1tcy+HQK$(FE1I2)VFM+*1qlo_TP+J@r_JImxj|sIG2G^U5oi}}tdcMv_Fgg3 zoq6W;U%z2w!GFLX)=%xx$M{0JfNtiAe>G4a|2=xW46Dg^2bB-}Jp$aS5w-Bkv%M`e zMd72j2Djy~C2yvtk$-#K?=TToTu!5eYIN7+-{az(cbb>E<~XhGlQ>u_wPI$0zEkzK zQxBv&h=dQdSB#GSk%?h3UP5TLt2cz=$ebelR^ILicoD~Y%rr3Em4ksl@kjGy4!p)c zQ>Qvov*eJRzdU~CD%T#x^yr9d8&+Rr{dY-lH^myA8*S1qM zE&SE-+5+UUh7-_d#tGDgy<)Dq7xe14s9L2Xrb72 zX`H^>ie-m+sj!0&r|k9kg3?ckx-t5_f9-gOaHBC$$Szyr^`Z9O{o%)Y3Fq|KE@m@X z{f^7Es`*+6*m?qVJ9kDX$ObgefCf77j{mV=?HPjR>)>66fkFcaKP44kKvNN3bts*6 z)NO1Oh3!%74Hc4MNmg;7)#fGJ9ypHC5#4v0W3IA!#i>#rljjoh)r&#cUEC)efH4whK zxaz~mw3$+2lisU^K95Bc)=A__r0xWc{fRUu>Rj~MV3mGk#xn)&4m12y(58X3tI~V0 z=P4yC6*oA-AW=U-RRPix?`u0$UE_`#5`#)|9Ylu|DSqom0aFVpEb*^w+rgw9yBJ6F z5V(`em^dKyb#eM=lQK1zUXs9G#=NrtImq8O{L1l)7hNC_S&=BjU@CWCsnfP}z0^H@I<95K z((-TcXWcHVM{`DeY$-TMyqhbcQSJu%_hjV*B=rgmNLJ#2C{_G>4d=hiOa41u{oBmM zTb>s%X+_%l7tebqD>Eu<)dKMpC4o4lEHspWPtOaMLJovBfj=G-wr`}M0BUbx?B~vP zqDeu+MfOh^-o2?0;Fke0Vbr_U5iE~fnuq9kdNkId*IM|4pz!-^P z?$o|zT=++L#clZj-xCRxW<*}1eOWeSSo&XodZ-x^+LS`vmDa~YRS))Q3znFp2mw6Q z^k!#nZc~+i*V#D)qI1-|sut8Ag)ze&b=U5HEyQA&zPO`L(V1GK_QpjCf#@q8DryWu zU0>=CZFpniUf$}Sa#{mSO+bHhQfE#eMQVC?_e>TG*OAx7?P2~q;-V)qmW+Vc&`&^{ zFaGxs|Cbj3f7<&gfPLEfHze<48VJ?!NTaj|^dMqZ$&Jur0SSf}v$~NR@(21YZK3=` z->b+j2+%ww#w^O9eM+B0*G{h0}(Hup>b`|9AqhPn#$T zZeZTMdNOVRetQB|FG8Vc3YgUka0nyMM>9h23sf=(TLa4W{1!9$<&Y#*iX7>OU%}() z@{wrGw>=m>e!A;5|B7jvnsY)z=&nCupdoj{;1vI%PvBe5Gveo23)kkNu~d+ zkp(h@9Ri)`$%3?@5Z()RWUlUKl%qbo6ogo>HdIk#fgW*iFr<~;&*na`KDI*_HoM9aK*1G5KRS`_(Nfo;dFfUqrVP{D5(p5;UN`_ zEtxN?VwPVX9xc2*+m~7WP{|kqQHSW`ex-x>>u0HmaxpQ~YHsyx@jvK3AfgVwLpZ$1 zZcw~AH$357JWhhSc#gPulsIEQ5pJlwP**vvPu>M2)g5d*!*kf6W#e>xJIEIF(lR@+ z0_(F1d5|nYPht)@Cf|h0%$u~#--9HvryQ(Z%vU?fK=>TQlv{|H`2TI zB!dqT;MwdluEVL3iS|yJM&HOGUSc&>DJ@5Vf3y;!+ka>I6$w8jqFIHHroC;bwi3^x zY+H6vAe6LzVB@oI5ne2L!g4p%#f2h3(@VP%RsqPiu$HS`}|rSwX6WdP&)x4Gon3m3@Au&69fkb@U(n~ zuL+Go9Ax^(3$WT~o#AjLW^iX4Zm4Z(?rRsaCY_#01ju)4LuxrtF2N^@JIB#y zR~j|bWXL&e)JvO>?$?qvkwwm`Jl32y8M)S-n5>tQ8^L-mmHSc|-{THh>mYQLdW^ZB zmb)*{w)fg^dxpEbB|0Z}vY1i}A&@b+JS<$qn(Ln;7cLs2B}71coIhT#*J^v1YhA6?8H-^25r?V~W30>v+%$J z?(GmeY)m@@!^hNZsD-?QbBW8S(sdBzU)4`X0eG6=`5;YCLD=e>lm>G*z-^QGw4Cz| zqOr}=ng!NQtU$-;Xy)YO^k;E9Dq!;~3-ZO(hWba3KU)%a-*DHLpHxX$hvzJd z*NillAIQ=bR32!tdW;Mqno)~c_?A*R%$u-~5DoI+A%BONUIZnLSsq|3Nvop-c-vkn zN85||f|_dM^^9~KY~4DJKk`A%{QmV4)a9XnM|4e~{oJnKYN+g}wE%IDW2lH^9AVz# z606M)*P1{9yk|7iSI|j#*LWL@S{YNykPW-fzL<{ih`LfU2|gfM{C0AX@OJv_m7C7*VwF1)lkiU7r?QlaWPw0uSTlU5}3=@=<+lmI;^)9hlTyG;PO@XJ#|a zZhum{MbGYn(cIgsf_2Z?{a$SvH^42jgGIB{O!(1rlUGUz(ShhMoTj%W=p;CSJ*QM+J!s7IyjtiMKufVqc`qxXJZmZvhABH)k z*38`2)k2Y2t8z`AGjZ-;8==t?QlFUfdfEx#tu&Od6eUmNH?^e+!ST2=jzjbj2V>pD zi|kicJ2sALAcM>?WPI^)I?gpe^$S}}GH3TE*W(YU3JZ@3Yr4h^ zfmUKq_qm`1vWVsu`lpoAuoQhGRUP*(uQOasAL$(L%KfnBzWH3-@UZbtAT#k8a;bHc1{I8HEuwfWNG6#>_!>+f$@F81Ag0zSdcCKQqbJ*W zci4;AE~T@F2#`s%yF=P6>+|9l5N0%Go3qJH!+k`m&j`{n=$^D$JwvW#cIFyJn@7kq z?QGKzeH&J@!}rZ)vu6Y7(e6vTP?_dPHnIoXDJMC#97{hzr$h^<}U zym#^a1-jbKL^j;EtZ?y_K-!1+Bj-m?%Lma)&c7FOPw?zemjL{y0N2LfyfXfB1^^*( zeFLjMHWjlHrDN6^0oHVH;qo49l1S?Wm{p6hL5QqFRkF$3$644jtIZYw`EaAQW{4~w!uT;3h!i(I+AOS05kWzUw00S!O1<-$w^0eN z8em=!=6Js==>*r}Js9%ob2#CMG-NNVxG!HOPOYqdFtQLw5~E}nk%jpTI*!~E5tFM3 ze)+uv_|rCo!)MW zW4b7=kZ)_R4n?7YUd=q58cjqye<_$^b)kOPbGBa8?Sto}GvEQZAuyI-zE~>bTs`a> z$g>aT>Fdd101Z60a%%cTy7hdGXwp-K3Cb01$BuYcgwYnv%OY@tR=TY;KB2+-n8l^5 zu5ovXh0OhfEtc#&=s|{Bj-=8GJOQ*JvsX8OE4WgJOmhQ4)JiaiXJ{3CI9-sgLqRSM zn@9&P+og}17pIvwf&Zf^mQL%3WE$?lRa}b_{z@ACTvCZ-#MvS-j6vboo$0c|j$~BY z_dO1ASmqp-ij}kF1)WuGZ3`Ed#@N)TKwD&o1wLk(l}OMSkJa^Je$P~-~ z75-um8@@u>Twyv#ICEv}67$=ED|VxvE}~cYHExZrzqDju<1B-&;emjx0SjJ2{}=x@ zAe^)|q7T?mymM+8O?2zOnV2wlj0yz+ooRMt)zOVOxSD3eY2?$Hkjj5oxO544d&QvA zExg32SOgB396Uz5QXVSL-%d$?Y?LSddgbf$c&cdY%?V*+5+*xdb9~xqjgKAC-+O8*&ErO zqE79|AqZGXv0EdI_N)LhstVk;@Ul6ipdn$%_IOsbopG#A-6K?}yLey0gRLeZgSquT zQ#$;a5N*`QZ$xYAS-lOu!S80oH|#~>(5-Zx@O9+sNs%aNZs&2^mQ8EMSgZK${jsaZ z12Q@Jtt4BX?CSk}CTAzhkC5>BLB8Bp>)z)Szwn3W=gYG;siU=rX7Mw`M=R><6ykNo z{wGcF{!Z8D)7Zyd-pi?XL}kzR{cesAUJN1+J9AzXzP20|aKEnw%xCLIJD#sDaw~yN z0huDLken#897*+{|A_8W98k6}HZXYo%c%~vemoqgj9auM=_s`!QRwAoN-e)0$VOeD zlFdwpe(%FYFHr=_hvwK4vUY+#wH|btT3MUsZ*%%c$dc&bI2-Mt-@TI7MxwFo0YW3Y ze*E49$o`a8RN#3_JCYa$x#*cn{APG_%@G6A%%0nJW|=^(Tp(bzCY0h-Wl#|U!LdyN z0RIssWU)a$|K3z<#c_hpfMYF7xpJ^+cy@WgzOclE={)cdvD8qj{wb(COC!>~Ojff8 z27D}<>3zsq&Ml5K9wex*?CiAr1qr9|;Xnv&X%ZNyCg{WMv7+a~hl^jsR5H=QD zup`VM$BE5LoVaizQ}rK?&AStHc3EPc6HrItRekc3rV#ZtIOFP6Xjf>7r!ac21PJ^> zpeH0o^waz5u%Qy_KrnrWs&_a2HO3$Hmf(E9)`G&l!INxok_nEulN!pLBxpZzvRC5Y zRwO@z?(~nh7@EoSNr~>fz4j6RObW#OSvecV;={<*&lv-bTOVL8LLFI4k30!CncG9c zU}huOu|R2;ZhBCAJng!d=_&J5&bqHtD(YlcjBZunT>I?%km9Uc9CC%73ODx)xlLfSw48JXnes`#-F00EJqg>rz z7QB^-Hs_Z#?8`jrLpaPi4qf$^B9mjZ*LsyZ5WEMI9^W2(k={nhXFjY+9o0nODcWGq zwsnfjSk5fgK3{CPXu2V7)#hXXizxeAB;3$1s(xsLhC1dh6Hfhx>4;AL3wT6+3I4zb z;NT`G)ya}ZsrJC<3YJm^qXj4!C#{KPKcXK6i}PfEy#> zPWm~(K7SPL%Vst}K#s2s(aVYStB5Hof2REuHSpYS%TN(>oKfj%nAY_GoBGOuqoaLB zd4jydWR@iZCGrs!Bt*)!#FAL1q~BLda{?{(o3*zpb5e%SZU>93xo9svgyMV_5;JnZ z&?aKcsuQ`5ZMl9Jw-}B4cqS9bcV9nr<&Rk0` z2QGcDHEZZr`D#`Ng^kdyo2E>42v{Zx=|PO@i8q>&Su`!Ml&C;pseYMkjJUrB!9t)T zj!3HP52Ka*E35MLxZqz6CTd4HxR?-%>{mIRqKS=9_iwTg>S0?KlJ6gwxjjeg-RbqiVJ zi_ENzS}il^pLIyWItbLiLkX$WNt|k81&$|?S^Ez6e-^%>_+VJ*Dd?_9U2yVMn*0n$ zs2QPQT4wbOCKef|!V2H7Yo-1cD0_+iJq3s0_D4YVSM?OU4Y~=#^)EE}?~1B2s-5_f z{NZ{MVUvn5QQ@#{M3sH$n*x_)!D2tZv|QrO(pVEXXm1Z@+aYUjHc@ZxOB}RG2eun7 z%;~97)1hNOeUWm|O-egb&SO2cgP_EAF>EpMd7ncRRO> zHcmPv6ewVp7h%=Ds3Dm|+7?0~?wjT>BC*nG0eNzZMjIx{8Lexhnh`*iT+GA!%DYTt z&P2JQL{8qCj60OeFdxg&hb?cwQWP=9tDmBP{GbmXIp!TFMJh8DMuA@GsYDH~-EK_n zV-#MH_Pwl_1jGzNv*ixLs#;D>QMwoWS?UnYKbunNd;Vg_7iSi(h8Z%$AU!-AgCPRh zN6ao{6*9+}rQWY@dk0|@N3#J5q){nkhvA@Fr|>w7?foKM7L z$@XphPqsP?xfy85iBvd&4lSPevKBvk(HSd7A4Q2`Oe^c#3{|ltul}D)1%H2eBfJ^=miTx*=NZ)js?Q@u;4Sj^*U9dJm3|b^JSkU2ap2 zawQ+fb*RZPWQEug`gb#u&(~?30Mnhk2i=WV069(AzgavG(Q*}!&P}uN- z01d9ReQ>s_snd|>S6AnC%Op&v>84ANqf8hat>tCy`V)y>6L=B!yIo7h+#xWXgSgEX z@|#36pR*I0yB4QsrxfcFwqN{qYs%nIA-7B4cD0<5kW0w*aj#|SFDw`tkF;^yuDP0) zskpX>&le54fJzM$oT=AMK(aqH6@ci38@tAdD5%wDeaMd*wUnz;i$I+y8zxobWDhVJ1`6D3{)JqoU z)8vcf-MrdV7%wJ@8|R-i)%iR9i$C9smuamFmV6@Y4ZG?nCVJmN^(l+ba+WTVF?7H{02y+LwC#31?6ALIsH5K8bsNP+ z+7-jz=n?RmYaHGAy|XhsYq&OVNXCukbjSy$#OP||PzwJ&Y2lItOUgadOEc!mms{Sw zPc2HaOBB~XI^el#m4c>vg}5Oh^Vwn&2Ad2`t6*H(ncAbH&UM$<*XFx2ON5=4N3TUv22duy5G_fz ztv6|y#7jJogXvDu=wET2VT1NSu08lQy(Xv%a`k`Wq1t$m@|rhg3m&)pMry6kx=UsA zzpC`CYa)m}j+?OLqv-SF5NhJXcp4y6jr+w~u#)n<;;RVwI?MzEBUXPP5o}Y0Relgi zm#L&53AYjWi`Ov5Ih^+f^FX{x~DwQDl=h6Vz>Op)9)bT zA^}Ev6Znw_Ipr639tiBD=EY+jvptx#=uyK`dz&7|y&Z=c%Y+`N9U1^hnD@eQ;*ANUwDn1_<%;nxG3%CRimlZY<0Cp5*KhM+|gS+l}a{j7Xh zx#SQ@5>pD3n3Qeqd9g@w{&V|vOZ+o8Fhmb8l$-w(1v-%Hx@P_{3~v{xl^5*rj@AJx zqmNmaUrg=3^HbCBBY64|mg2-+S`BIz9h#*ZVDuV3Z0uK5Gt9<`AhgrE91Lh7KHGRP zHb>YjKaq#W$EFw5&tIKCUOkVGMRXJVaH=&-q#HLp)RiNl#x{_qDjn zPt%hSG`9Xsxo{QjJ-S)*hOFMsM2?g`In_qgkMbRfB0Qe z{ij??Rdc6*lCiwwr(^)HdO=t2;mWO~3Wv!w@|=FH^8PO*(Me}k8kne%HQE}=-maO2 zt$9jMjpJV2+&UC4LEo0e^$&PF)NSO{RP?B5yja^s4IiN|Ev#vckr*(<4YfIf?I9pn z9geOMD(#_0SYl+pcqX{;qIctp)-Kii=@UR}#{u>R=`dS#7@tlH`$$c!X1${q2rI39 zc7revzd&K0Tj}bW;^rAAM=SWnV)3nutiyqjq7 z1QyNj^YS*agl1wg;CK+Jm#(G6I1bSU#?uJqwAXRFs3(%Y8LtykeHTC1Kq+W)DNog~ zDcY61AlztfoM9WubuRuDUr)2_o*R=V8KUmSw~TJiuB2~n>Jd=5gyGrcE23g?J2=F} zq}G4ZO#-9Tx{qP%LBKW#blRixQAJa-h|RfGMz)dkQF-Nd-vj4N01v_bZ5?~>N_VAR zV~S#?Ah!X&4pHZ?-mpo*KIr9u2iq`!gap_B?+JmOzM-YQ=^s-jTbm*!dD#s{B%fnd zVfRX$5_mL~Mro;n`B*(f&wV#7MGb+^S9+KDVl+2(<(BTN>7A@AuSc&# z_wGW@r{ip2eKs<)VqtTT*|b>l#Y#$|(Xv7+N?$e2ns4Uvqc6QdQpHBU^tLmtlw8G~ zErDko+{ELoStuE(64^VtL|t6%pfgGY?W0OQawC7-S~pQ%1Xv)?iUL1T@~AsZmlPaWs_lCI~4_Q)SW@ zAs|@Z2O{DnrbnwC*2b;(QSQEMwoiJfzt?375_6}C@109s+LDS0?aa6 zG%zw&$C%jKkLE*}B`#TkfP9E)W@L8xW;r>bsI&{Xl5I@yQ-Ksk{l9^hCYe>!2GT1OXX(k0D%9C~f&;1dAEw7~4~M zk`6=*18M%=wCZujtA_^QSCKK^1udFx#}nl~9p>BLUycobmnC#^wY$)2+|i$y_FSmhd%Xc!cqWS8nB(>Rh{isUn4~yAXDJ?$n2ZmSm33p0Cg%ws`Ivh+`3rMN zX$*zl6p+%~04eQn&d2~e^H%O2#`H$?PLA}}`gXzb+LnQgSizs)s{=16;Z$m56zmlW zor(ZGx!@w_Yo4{U@!6`c?r)1XO8i?vnfTkzv9SzWmuOj$bWp?)2ZmKbSxFQFmUww~5GPtLPvI8L=f_ZW&a^TAoT787~6 zRQmi!;Nf>YiOvgwM!6#_&@npj^}dr{7nC` zGQ~FP_*h;BTa`foMpOt_AJ=s!wiHm%nx-pg;}m)oY!Oz`;!o}biaAVPKrPfkqFjD# zL|)ynK;uXR*?&hF7w2QY1-U|9&mOln!|?Aj^xCuUEb)L$wgdUMx{+YG)u)FRsqW(|szH=(9K!l9g8*urbu$n zg(U|l#zg3E#3X)@=ywAAp$@?ns*=QcaqMO@P@koTaMBSEz%O7niJ732bR0lfNU7A= zpo7ZSxrbnS27SoAKYFX`GgLWZ!Uu&hgikf1frqW)JgG{66wk;)Nqhye7|I(cl#Wq4Iy&>@_IY1F5O`BqlIDdXG}8!jBr&D10`Sim}nb~Gy z-V$*}(+#H%Bc503`&1#`EEw!kIBwKsK&lMF4PC@nMwG**%_^#5!MmGYRkRiNxV6sj zGJyAZDt5|=BN~k(9?I0@{O}aB{azLf-7pVa=Hv1_VU8_tvWsRVst-b6bzcsXAlo;` zEp6c_r^(pUT;0+9CRs~={r}MR&X0XyTl;8Z+iYy3v2ELG8rx1|HMY~(cA7M3%*JYL z+uu*7XXecGyzl+qJ9F+2`w!S_KYQ(kXFXeieqk0&(!3@iywc{q!8*4Vhp{&`nj(sI zR0T7NxG`Rw8o~KHgN? zn7dIaBwiOcl^U|$%I!!f8|FMela=NQk=_}P7WX-1g%32C(g9jw_kg= z5b|=|9P&9W+lV=$ zEP^MW3dE$y3o1TfD1Fen^qQK^6{iHl$^_Pj4mdFF>zG3j^R`O!aPJ+Tu7ZJ&^wOfu-L#_Sz~m1ZYmv8rfLCH#7a?_^rYPVEp+J17C*H3zvo1c-l$Ch5b8~9ObpF1YAsJ zTZCWQb4a*ZhC4lmZ+P|$1nX85YE1SI^(k@+qTWxr&(7=WOyh=9-g|r)8FZs7E;bO@+#4}EzgPzXIg{&tu2El_1gM{o;AOPHtX{8mg9k8fM zGY|%&ds6YRi`tAussIgEja;&*NhJ z@>qXyb2oJ0=3*{6sUvIiTFhfmmr_D;-rI6AZnhM^O^M&jk43tfr1ZOe{8(%j3L~1g^Q|0I(5Xs z^*{D6e0jb_PwwdWC8JyBA1!^Ia-{_r^>4PGf14bn7qql7viLa?|9c!1`AI>ln*kL7 zC@=V<9*7(-&VjK0eAy10xwBB;c5!~F{)piMOobM1q__CujbDWKX=g8XDD=7bJGex4~WE_pnVOQ_A#MiEA>^6^r=y<0r~A=tByE| zioA?32`$kTy`0IqcYKf0x{PsyAMW^)wb!;KTZ(rkoTnP2a(<0ZN>kk5jsgrV0*E#L z=2x=+Vd$Umm64s0?B)mfGA}2IvW~6H?L-v8Jt1s=)U%ubb;U*P)KX2vm*sXC%r*7* z?@iyIr17>=UV3M*$Ly8!IVnf;++DX@Kh2qZZM5zYo-O`&eZ4fj)n!z+yQejsJ zJ5F1hc>&dT3&+T#w3T_*R81;hEoLS$yJX>Pt0Avw3Y?JZvr{H=Y1i1-xAM|*_%qk( zsd*`Ec6WZExCV3?^}_=!_7(t7ZGiRu#uV$HNX0@101X%+O9Ll|KhI+S0oQ0on~w|t z{};Z4@_A4ms=&0HKbJz1*|><4AQ%zVABqf5@#@QsC9-5Gyhvh`$Mf;s{M+^RT}u;e zq!9_+k`}}mc{hL**=S{hc$m<%G7m@>?`}DP!ASj7Z#W}QoRN*lC6zzHrU@6`@we(z z8|DUg;)zXDzJV!~-yHqiNJ$;*G+R1lnipR0~^j};nyYe-EA!lXv$ZBwg2zM&|% zg|KTkTMS{b+DbQK0?G0D+K$=y;=2e9X?50vU9E}E@^>A!vNS@&VRF6)DcP(I>m!RD zD&8-@FbQxZP$Rbi_74D@h35ZH=kOEFR&p{H9|12jy~j~*;AD8_c6nderoKC5&LzP$ zY5f`Ty>t%k(}|81LsDnWa20cHE8nFVgqjmA7(n^8qyaS=)##4S$S;pgn2mvEv~c)LWu;-2Ft`aKv&yaR>;GnO9Z-S?oMJ_D0da){ql=-3!!D4 z-T*B7kDc&u0RMmCgy%$QjbP7m9G6vXWj=5s^}Za-tS{v9VxHUDAG_F}JY~3VCjWD<|5yW|RVyEIT2}7J95S7V=*K^z|P%G3b!zyeW)0bNX zw~jYqe#gq$Uh`_ARiJ}Fo)6^7jOrW?AeMN^dN?I<*dBVpiosPKHw|=OGh=dwQ&imJ z`;W=Fe5jp>b;4yB+RuG%3EK_H663aB^c4exx%LV&Oe~Ehtj!$F3@pvuP3(WcYqX5qM}EX0KorU*f29vx=z_sgPz;9* zA9+@lqt2GYufbB#_i|i?D)}9g&BN7nJ7o;Pom$gkKg^$6V61?Kfa*7Mumrtfjtk|#Q3L+#es zN=I`Hw?1`rmP3Tbc`#}V(6 zL{DRFubiY#e~jtQp>21WEaO(`Je+sFVBq0G=h*-1&AsD!J9T-Y{N?I)KIix1e#&7e6LW5g@!~N3r;=7vx{k?yiK6V-O9#D z+FgzuGv}g9yf$q0^26lOq3D}we<3`6X}!^|sh)YduB>86wF1bGp(Yxu1_-48(IIv_1r34suDh~e>Y zK7A^r%x}GT^y+`uY9v>**`qIaO?u$kz4=9kq%X0(=amb~2*CM&;gx#@`!5nk|5L0X zyf2?1Jq9kqBALfBsO-ewBS7JcRDfna{jyT)7my=_)P1+?GIsJv%j;w+H5Ib!*OOmX z7ZOmyWT981`R!iuoypkI58O-D=&J3!p{r6x9*4$Sw23rG|7f4Zt+l&%=+7p-q9-KS z=*zfCPBRTAN~aQl`3(?{1z;^n-vIbdgEHsNC{<*22}NJc+FP_wZ4mYZdf95Cm2Ao? zFQO=-4&<{mI1*wQU2+sfeh)ah%mEJPDXH0cTxd^3&PL5bSh0#DaPs4v z%$%^!#=@lh{Yi#Zm>)7`(2etoN9~2k+xO=)aPdpbN0z1(gv+cee5~yQ!=v!diFS^s zJ)6r9-&0L@-Yd{9DV+-{pi12pN3NnPc~XD4z8qxfH#&tnOtY)&*2T;>gWmk11dhwx z)iP8;g7F$|P>sKCYWhVu^0% zhVry87?*DN-4ppn*A56(OD}P~ zux&`%RCYBhG{gKf*pCSk{{sT3L%4d$m{fKEf6f7f+W}7b(OO}pjPfsGN0P0z8-|lo zmlg6#bz&bRyt!|z&! zd9Dj#BaOB3$HIK=!D6mT$Rf~$7&fBA&P5OXlVRC_F3KZb(s!*HUXQuj+wgN>Q%>B0 zA^{rh_VMXQKd2HUmolDiOXu0>i)Nz?f5q)2+|AA+0Jr*pWB8j&mH$si@Gl1(C1nx7 zj~MVGm4!3%Q9E5p#uT{bu&WLYa_-*lNdgc~u2ao?T}w$`c@*6`}=u zw43hzTFi(pE$PPvfEfhxZ%;`7X-v^bk8hjt$duuk@!Mo0+uSXD z{mWY>Oe6do<~BW{i#ZQNsC{#n;A5lK;z9K2wQz)x7V|lz0@=BhJapaHNoGW`}*I&`14v%Ygj2QvIF|3cVUH4f$@``u`}!Wbq-=viSMnC zg(N{h&jr(?f!S_WZCpq-YG}R%Iaj#A#>E^?o7Y#44f@p2NTu9PPm`tM!pOkwq-=piOzp;<{exB zgD*8X82ikMBkKkPU)a64cy@?vTCRI02Z=bqjg}j?u5ML0V1c+UFtMpXs>MDJmb))# z9~{Ra^PQIDB+xYrHUN{%i%>SM8g;#KrJB7N*b|!Sj-*GCOtMpw>|0M}U%$6sK&8ov za*y@=zyXcRZ*?ZPQzx%!^=1-IIJeMN0vDYlYp zDJGF`=>;sJlkmJJr{PvtdXsJrYIG)CP6z|qg=(-JT3P5*7JS+#j%IOh=$gBZY}kcl zjYr(^&_&=sjrW_etAWX)EDlegji%(|J4v&e?5GbEl0ij0H4I+}WhJfJEm+3Ak9u1- zc7IUh9cdB>nY_@Xo*5b^l!`x1XAHstX7%oc5rdt)!Wk5~aj3MzfwHT{MfMxP#B5(@ zeqB5{@f|1hT<6)6rOHp$4K*5y0K^5y0fSd?jP@QpFmAUwSw?KBgy@;Ns#G#KgC zUYNrw!*82Uwj+t!Z!ltE%@x(fK9&K2_|Y0Sk(x~=)~6_;s3mj{8)t!?>)9TS?#I~; zFvL8HTDBI0g_*8@zj@+dN*-bu%rM#^)?}9Q>USdp4~ly)sM#4|Lbf|;OP%*2s9cth z=X1WR?KvkR`Vbu*V(A-Gx_zfEr%S>+fdrc7Ks9h^`x4{!O|wGWbC8~KQA*;o=jfR@ z!!F~6rMM<{o!y?p`FQU2GS0H$kQ)B3ui@Hgpw~+r;_Nef((gb7rQ3o@A+na=jFBOx z5}O`nGH$V=vY=oND`QJ~wOp#h`wJdZKLk@W>5aYt{4{MI5efnysyGV1wU`i}IZ$N4 zcm}(7#j5;a+5f_7Tai>QYin(nC6&`e@f2y}_7Z_5x|{?-%rn;$h!CtSH2Itj${oeQ_dY{)Mr{>h7KyH!eZ01MXEDT|1Cq>0whNh6Y8hrK#cGN_&l6{1BjAo@&RQ}%l~+BW)G1M~*X08P!jNJj zrd<65{8FpsIki{aCtT1=kkOE1FjiKim|Uj=A*Flq1UXwMp5;q0xUd;QwX0O70Q*jp z=cAtMCO8p%aq$KB*fK)G+H?D(8qPECJb6#QDKCkDWvSn4<@o{U$G!)&FZ5BMlAvBL zYZ@*Ze&r9A2QPssFlfShxGRM!S3iiQL?U<*4N|$;A-AsS2GFOj7=^wLN>f?iMxmXF z7E4$Y%ust?gxRFt#E(MbbUK5P;8ziS1#t^^8xt6B0&N6nYsMfFPfgro%youHC~%dW z6THya|Y^{Jo7dYodXkoB%whF-6&&_y0^N3+(8 zpkyQ{;ejT67MgSgg6-Kgf|y`1j21IN%9eh|6137~Z(HDz604}QIL`UW1E(_bKV$rtLDL+W(^+#Wsr1?Kj&vN5I5TPA&L%q6!^ZV4Xv}BAsoC?O=IW&jng%&ur6@2vR(U+(TZa zmHX~2kz){=QZc=XsLe`EeVprlm0c9jDR_aU%Y-Yb?S>0!YdGUYTNxa|BSCH6s==01 zhf^-Sq=(U#L>2y`?w$vE7_VX`US%Er(kGTwNgvr#3712@T&(OD9+jdSZ)87)BQcbjErsvkabZ5SGPu0C&Oq{qLv|Z zN|Xs5ix7q4QwJ4sf=QI+ds-c=f?53G)$yY~eR+70AYXRwn~7h7bY}!)_mo;E(qr%4 zTpvF;D$I+DwHBR-%bTt)Nb;#KGP8GZNoG=n8FuJs^=5>JR2Hm}jQQ`M?;5G&4;3(8 zW$Ja%eA$8x<}!!I*@Kr0Jwgwi znQpUj>ww;MIigvq3-3--s|m4h*rjwi+1d zO~U6|W1q@d*r&%|L{x|m8s8@Ztk@4&f3sWu%JncdF|x5YaI~>^_=Dy08_9_QGhlTq z%V*M`Ze~wWDp(O(&<-&;Srn>HMxq3My%mQ##J1aI$wjUO=WQ7dr52-(r42>i=!c!@ zLr=tvWDrI$my3nNjI@o%1HT$#sF)tm%D9!xxGxsmnaQH8G`c+kGI4m7)kyMwIN57d zL>T;C#h{9JTi8V!Kw%C9tH4|Dzin^OOTpB&joP{Pi)7FKqWh=!eD`Vjb@u=){Y_z~ zUv~c!KMB#*08w{Tk(aGbFI9@#g=`(JK?fF{IY>$%fC z9zALA(nj+U>TBa^k~{VbJ_qm)eZ6aQ;S~-Ba0TExDD*N8>|DqEV%frSF}ZGutoJ|N zt{J}F%^27|vVx!6E>yCjCXoaSr`O!f?Yb{%7A&$hsBC5>D#!ZB%56*}lvogQ;Lt~p zdDti5pwfot;Y`G(yTP)YEqSILI*gXreTvkE^#mm6TUvaA=JLaOA7jkp`9ST*^mnZh zsZ;y?v>k8o%%?L=$I)8^8NM%ny^f4an7wHL6V3tFKajQf6M!sDObv|O{=kt*#2;*_ zV3i^%XZKV`M5GqcV5?8?0oUanRU=%1Il+v;9n%NtXhx$S< zy@6Bn*9ImnTH-b@VgbSYh($olI=0_X=SmCwy z_&0Uze*x3KEb$XlCXrng{tSpBFD4DL`e)8nsqm3@#abJ{7QMWFhA>5p&#fS#^k?&q zc4HY+dmy4Xxfis?e2`*lTKzxtBN0qagrv>261FoRMfpPjU#nZUo%G&qK+dDbk|f}K zl(1-&`?7k2Aeo$+02(F95zcCh~kigRsS z4)i9(Gp5k{0$ZLVyRd#r?Llrmq(W%87=ihmGn(4z@6P*2Ue|eoQpPX!0wZ^i2Fzz7 zIi6#up1(AT9pW*=hyWHd0MNq!CVu`zJo%G~-R}=+Z>$18Er|-LFVem{N`3zI_?aK^o_^3$k+#xc|tbnEQR`Z496LoonV>;3&)n z+z^t8$~C-+D86V<>=&f(k(i(01VfPmKlI*C5a@2XCL=!C4BPS}UX>Yr6HFN)ldQ_j z=!e(Wn^fhqy8iWjl|oFd)B(WQ8?gQ+jQ{5Y`3YkgX%2W_0>sCm@1W3J=6d=TG?2L( z#A51ojL$lC1Y}CXUTcmil;FFv`Z>!2c`h(uRMQDKLcSX@{_gi zG51F>M#zwTK39_M5}{uQPo|!JVS7;NM&Gakpv3}!*553u{*OEPO(X1gc(upMeF9ud zKvjDTu8C3i$E7BABBqcw0{%M>0(B@Pzc;B*aZkrwv~*PFHHD-k(uH5xY&5sS@tmf% zRR-E9O&8n7hmHJJwoO(PR7uL6-^tE0jV|nuf4yV8fsJ9W~Xbnifc&|*pDia zk7OGiG`1l{J&tP`9g0wey*$MXxx-C%0Hf#tP4udJgYHS>8w#jt^CEhK8}Wio!gz~4 zTK~v1>O_d2;2|R{%$=?9ovu_JrHg9-%G*IgV_5uLU2xMNWN!Y(;;tzQY^<5J>YBKS z==2Gb>a#j8jscnbr6t(W{%JN7%gfm% zCk>_u7$-N4Z8aZAS=oZn7``h_aF^XM-f1|Qk7xMfZADYAkz_**hY!C{ZEL>^^Bn@v zHVrtde=cbGZx`?{Ew|T6k@{%H*DopnP4I8oi4C$HzJOV9Vl*zn52ds*DX-?_}aj2MQk}>{8c|QdF=DaBIL(ahm?zD&$m^ zGeZU7F6T=5Kx7aYPEV=Lg8Kw7eglpD(yA$p@}_Pv&__m`BHeE%wCWqZ}LoIO#3ft*y zlha14^hMK>P|w1obS6q)_wxif!HbPbJ5xx{bNTv`4pyZPk}SKrvJ}ojM<4WTO9`fk zuz#6o+BvKj#_CvVsGoj{ntZOV>T_6c#y_%7X4TL@oD6h zC>^kBQmJbxP5}16QsZUoAW0ej`SJ&qWK5`!`8=idUaQLiDvV?R;wo%y?QS?-flGrG zg)Vc(;k>+b3dejO{5r2}1{AfJbOoM=75;*AbZ0JJ?-*)OjP~roZWFBN3n#c3y|j$R z7_q<8yiqZ7w zuM_;7rCmvc6*wp_%JRMjl-$Z)Jbp?|@ri`ZAvCHyT_5!){}Ugm;dXz@9d^`})7 zm37_EWhC(@w?cxc|IsRs*)aPW12AeEVExVMu-}3w8>>G+$@2%zUK>ZXm~MWrISc|R z$HH;WF&xaPw;u8}a6}wl^RVf-DJabFzW^cSDo8t^A^m=@TW=2EgZgM>ehA>#-}}JF zr%M!z(@D#GZ?>qjG`2Iqmi_|m-;O25wnT~~?{AU4tr+bkPFXq?eYn6P)abT50mtHy!Ta9_WXnS6|aSpPm6K>oV zEk-JPQyIAU(H`!5*_&RD=(oNb9r{mav-x0}E8BBu0u|uxD}TL5Q(Fp(ubdNdz)0*r zNj(8a{)Yj6UZ3)KDI36m#kVg*x*5I;LXqk3)eIvT`>>6yra*{@2D{HsCkj^EL4wI- zoe@Y#ai1@Yl4p54dP)1kDWrN&p?Y}3Kp0V5X{vLX`rEB=)xQb)Qq)l0d`zOD-n+=s z30Z5cqDfmJx;bKBSU~ua7p91YJUx zm@=dRpM?fo&9*IiSo6f7p!Nbhd?b>IWU;(h`ejp)E4d%rKuC*g%+rTLH*XU-|_HR(0_sbso?Ve7kEg$~uzv$cn0 z%jeegDX;~?wtmt|hunae zMxFrTVT%0Hn&((_`je4^m!qAVy>m;mk4>wa^ZEG)2cL_B`Qzh@p%G^XH)p4_%iT&- z(@Ge}d#~o^mro$ES~vo0myR*ZpA-v_L;QP|--PEoGp2Hin`%=As@mmAch)&1BoU8$o_% z{Y0LNo3yh*`p7!=;)t0^m%TIGQ2X_?)~>n4?xCbpk9O^*??`Ryu5^oAL>2O(+kjo0 zUoO-QK!z~p_{Q4(LOd-s$WTZ(bg_l}%+VdH1X_6WK5G1fKOjw{8mj~}6KK9=CG3nP zo+e4NQYU>$+F4CH<>?FFoD-J~+QzB@=zPM;p^xu?jxeV$C6DH_=zi*di!J9!)DrvN zbctE!!!b=WmZYZSxzPtH?0rjpM|hI!I$UC3ta}l+XG4w)N@`$6cKS&-ZTCXf-joz$ zqB6e*x{EUgZe1!wBcnCCbOPu&4FVMs%d)rywAxy&)C?D5`^4=5mQf}%QrfV+X-i6$&8ei!}Gm2v35fdDWjLqb1rA90)!;!*{%@FWYS9OEgM_paSa)giTV(m8{C`!6O^-`}fJs z5~rJ-*3Y#@D<@)H58nKoLY1 zP<(3!#*q@)9!jHWD7Z*@P3OXpP{ z1WJ#)M2ME+bBy>+6C2L9b(%#Xz?!vx#R`X7Xue{O)Dw0OUwGnRDOeS-k zxR#F5oY-y_`yo&uj$XhG(*6n7i}rBp)*R9qH|ztiqJIoaWU`P8%@^?S)bJU$1J(1DKC8c&QE(K2VaPabMq z)naZ}3(rOb=ZND!95HydYO}VN!_RMXh-&cLZS^GHl#RH1zwM=&->Vz)VZB-iJk%_q z)MZ4%T-*6-GzlErk+@h)6dB>%;GGglt2nztQ;J?}I^qlUTz?gcg@XOG%;4D=K@oy) z98go-FDY3zV#87$pXOG9Gfc%Vzpl1j;rD(J3&bqF$N`;%F(p&js})0>k&e_P%82J% z!v0x`VG1z1gFR)1ea5m%S=a=O)md_h$tEAux(1=8TRdh`q<$2c=AQA<{D4ujGOJK6 z#f~k{C#$zUMa3~WsCe?Ia;MIcdLFjYr>H*6D!4X~af!LpHp2y+pGMsEBY2ukCR5t1 zne=)XV^Ns&-pvp<4HZg*Pe$o@e1-Fmv)(4C-APt!mo2nH{eXOIl~H6jrnp~co6*)$ zQ8(&r7|-;bc;d*C~*|vdc&M%Af?mfy;UUTm#MSn@=%^q^5Uw5jLea2 ze7yRB_XWV^gNokt2b+i8OfZdPceKQ zyL*eG2e|t98dS|89qscf8-j^|X1?1|{kQZrquTzdB_zjsT1lDyoSi_^b&=09;7OD_ zx62zA3tDj%G{y}_+7ks{=Xsfnci>cyoAq&z$n6>G6>+9b=h{Il&O>eFOtwqf#HO}+ z2OxIL?P9}NWm{wi_hQ9*p@c>845`XKUgcAtmxM zZkoKVZm(HMN*r6D;pSa%xW2d2)JC0&Vk$(`h=skK;eg(PkBiYMtbaeiyf`wS*E}37 ziXxW5g1oxGp~Y1~)KY=6rlCQIBVvm(R;Dn~IIY47E{<)!PU^n%EmDx#YGi<>=KYbm z_?Rwn@8OIP*WK7Oc_H8Z-@|zf|Y7Z%r9G~~x%SXla-%S=tiW}&n zrUyzUy0{VcWIX8eb+~HK++)c(og_So?hYo_uvbugu5n{u=4`)CjZW1Lt>y)@WYV~d zJuAyOMk)YZUJl-|N*J`JYEE^2tu1F@W!-t1nQQ!9|L9hxBRb)LpDV^2s%p_QV9mGG zBYSYzyQQ&C$cJ%?grco^{0_0kFzu4Il9W9BT~VV59#h&8%6F^kx;>^VWdiFK-yA3O zhCG>hPF4sajyv!$0_!Zi8DZ@MbL^3g5-1L}0mm`(cne<*l5^MsUZZPK59_PhlE;^1 z($e;+;7sifEAC;aUG5p}MNmEn45E>3@5J4kNN>IXUZqBn;dYFT1&K(V0Y7OXs}FOc zXrP7Gl$#<)cCL%F15n|f8>|b&Ba_W|G zDG8r1cVVskE-!Z(gJ^7k-dh-aYKe=Va@P*uj>x0bT{t&?AQD)doKK#1!nX}%%DcJq zVy-WA^BL$UK_j_x93|p#pmjXwvU1^GREx1CmlNHvV&ka^Z0KU$>#*{aJs{#P-_b3t z#5@b!twIL~Mro6fS8$+-z&4ykTk)+(4w`?Uff3QyZ9S+k(<(2YvL9FqBu(9(_$+qU zpM1c!%+9QB_x^ed<@98}c^GOL^ic~ROAl47Aqq%^oz)_mC&_hL3}|VsUEE=Iq??R3 z#q(MUrJ=lxb@zG*FXgbH+$Sh?<`4^F{k?aV)m+LMu^7MgaD#*O>avwXmzTPJBpt^L zRulhpdbiF&t4P2awhq*h7^ZszND-Uh7@N(ExwPDg)A!KKUN?h-;Huy%<9HMai*c*+ z;rdV#5cH=HKIs!3P|9!(88bS@`z?8sTvkWlyi|u&!$Q8Xe3^!A)!bmw7S(_6oUVI- zVmdS38Z_L1(F#yU$20AlROQmnpVDQnuBUXV4+eI{kYBLTZ<1WYUKO#QuZQW-IS`n& zonz%4b=|4o_;&7_bQh^+R1Gr})}53mEvnOKjWc1a4G_48{>(j7jhG%$`s8h_8uFrg zW`~K;7-+Y#t@i@{*MuW^I=UtUkZt|LR)vWJ69XeFoso^DrHRpN`}U8F(I_)yod3VA3r6MRCqnGbrXDK^*9AhPn3)f_DYQ=Kc5f+wvnU z(+g`8n7TYHL@B35B4d&bn-Kbrg)n>Xx5VdT;w6pO^^s3T2VNkW`52vAo_77qk3D9W zNq#oIB%=Yj7YoGXBY;l{piyA(R|7d8&@K+Vu*fPWUDR|6%zPd{_@#?Fx7W4uS{C{S zNWlJPmG?C$|4&=|4AI-8`CdPe>HRWP&T;rm7|xS1N@NU884pV;M1`srO52=>2O;>y zV-()e_68d6#^YoeWfvS{Q51vQxDwE_(CF{%ODfzkF%(qMmZ|v&`9GBQm zA~t>-O;Wrxms)3^wYU02_Z7eE?LH-)k=F<89Vxs@NOFIA&Ey9G>mNu3{9#M`e;3wd z!g*l60-C5VLtI}-&Mc~AXEkvpDVrP-gVpmu7Yazq!*5q3(S)1UIF6gic_I*IuHws* zts7Y5qoJhg=R*42h${?Xdr+4)aU2}2BOWRtBxOkgBA_~A%G>*2Y&qJC5{G(K0%J*By6dE9@%mj--!AF@R-?Eahi`+5<iqGxk4r*v3n@!J0BDl?Wm zELVNTMWwBg{%kd=z0j0Q?VW?Xu@XM~%=9`%AnaRK+wZwo5)QNlnIozWaUe3hSV45IUBsTmPxl4TSElB-gNnz| zZ(ON)zMbkNnHJYgEV8jz+`ancshVvd-Tn@+z-upA_P=O}pZCfn;`eSyc{*~oiA2E# zX&54kqdq@uj=hc$G6t7K4B|^h60EqdQ}@H@YP(#jl{7D`vSl)SN3C+S%xsbxTT1Q6 z$RkX{>(mg+K+5AIAxPr*x3bFI3W;0Es5fpqdgnenM!BX1IY(+Z)i!eEeC&t`{@+Z| zIH+s9DY$&K){2&9iM-l7|LQYCCot|LOvNID-k`ztHjhg)09QOfV zoCa=jO^lw(8h`sxc?Y*nn~Oh3?0#X&gR??p<8y>FAtstJIm`Cfr@pMa>SGGPZc~8u z&!4wH%8S3>Bb{)@S0Dl0G5|=^oa%)Zhd+TdUHIyir$ru5`k>*z!mZk^^6U}vsLbg!NcQq_%gYE?khK*8 zs4;Ub84_m$v+;MU2m@qXrhvu6=>b;@oS_V;9qc*{ZwTO+#lNPA7Cacf`wtX(b7-gP z0uWRS`0hgHAEW3GEBxz;kCT%F1UrByUN-|3X&h9xb};ycFCSvU^HVi>TegD(JBd$`6`AlICBzdj33P0gL&QWhGf)GR z160l`2(A&0Drt}rpfRd#AwZ~tAuuZXn<^u)u=X@|71v0i3rvbczcY|nq!cY6Ta)G@ z5DzBki{n7WA9p2Q@<=#EBgRE>yhKOG%5oGAzhTsOBp_{^MO=Mo7$1O}WBbx*|of{i6^dy+e>eC93un2jUw=3y? zh{tp#&7mOy4zdNXg#YnD{yaaGPeZS2ps#$A2t^7w|M#p7c|@P-e8K3e#f4H^zDsDb z!J)0MreG0*#-L?vSbVDBTa!P^wZTcwB&K;P@TXL^^K?%nJAj0&K5{W}nr-la_vZO} z2^MSILw!6r(MK=JmvoJ!i2^k^T*^U0;!~-SO7rOWi@nRBDe+f<(iRa@pPrz%;*NWu9 z6J`Cu=+FKGSN!7De_)Smzv+c%x4g0Bdv3$*JZ<6MyU_R#dK^!g_Ixd1|961(4_Z|J z3haKj!B6<7`a35~3N|Y(m-(HVU*+#!(5B%GEFz5amyodQ52Y=i)E`xN$KIL>;(&B#L%UD|XCpdj+# z#;q0FHUA)#-UE=xS}e7=99>|9{Ble=ABU(A;v(}s)#%a%eilTjvSk2vuGoO#iIfEx z+7?V%8*~%4pIz3E{mC9G9?-!aL-v(fN*G%NN?O+s##Blpv_`RgMa+V^F-8L<0T zGW0)a;QbpO01N!fH$_Mx`18X8CKW5+{UFx;@tqcDUBRZumH#6(G)>JpL*I+_M9+t* zZUFb6J9`K_X%X5V>?a%*I_6gi9xAQL!gPpYElXsNy+;c7BobusCVb`|tS> zz!cbP!Qqc+_J8*wI2z(L2EX`_v#u6^4^gt<%84MMnd%jFGvi{6Ybl{bRI+uk+1y=1 zP0&O5&4-Y={ua?MZ`=*D?0P#BU%TTT*@j2{q(ve%gG$Oe5R81$24S_}cK|}=`cbu9 z>-p%$=)30RH@Q{IU-?TUW*=83TMq!q&41;Z{BDEaeF)DhOoCpd)NuG=KQ1W`_Oc_% zq(^}0%5rxHuklMqyezN((4SiKnk+nl!Ed;!uprbS)Yi5l1UuOHkOJD?{(T0vYXrpq zq;F-;YZi{^aZboR06;kLviZ7@(fwtos~akZHm#Dnn)1X2z=+Hi2PY>>RcGSEqW0T| zQR@>$VRa8u-AG1>Y*zG76N`dD+R*rZ0PEuLiIAx&rgQi&1Lk5K;RMqmcG$E$w?WE`EPTgw~S6>;b)xQ&UKm1Zxa%vJc`wTG6 zE7?d5u>M&M;9sV3F|o9?u(omech%A<;`Ool6G{#?iG%~*(dGs&piZ(AN0A{t-H?5B z?$Syh;yK~ntKd}A5}NSeQ?w94qGUrbrC!6A0sN7fFvDgfMm(D9(zAIuu3~Oub+WAI zjEW}_(wTKo!ar+rYV)l!Jz07}S`I%g*UG}-D%2M>9fYG0p92UP-iRRLOF9r1fWHrT zc3!jN`&%VuSEmp+%8CB&|14f265`}+03;GXexuFR z^(^5rK6NM45#H1GwJ8qe#|A3XezA!>G+JG4yOP||T ze~QNFiu}hxaW}PU(M*1V!ks-9ZJST&hcQ`9%Zjn_8Og;>-FP2Wr z6jR0zQ-8BN`T$BKttV}q#Y`%Yg% zqHAZT>%wAcrElSA@X!0&62$Zg(AD`ow)7#n+TH$pH=-0i(E>gqy*Ge*t_DJI(;J9x z&ds$+bdzQ1ZR#BW>ghM@ka*;BT?28s&K*HtnP0oKGLQn?km;(fn-dtV_tBz2KvQdf zVl?gIJK)EnyCb9Wp#OHuo!z>kin!m1P(Q(V80sHj{OaRdTr9aPi-z95&rn?J&TusC z#o!$_!EVfgm{tJ4e{;~!?#Pc|{>6U-r zomHJ)EYgaNrpa5;Fw>889xAxs`r-`h+^RU5&9QXvGf7;D!@^hEno%9H(Mv|uNprA; z1f!_eygx`-;cCRcBs$R6l6d7c_;%xatQPd@)0dtar~>j#$!W{0XH-5rH%Nyr3QOhw zb-DhDr6LUVm*PEDK+IMa-ev_xa+Ur|tEo%r#67wtILN#-=2>_?5ePNpz8Ge|EgF&0SP^8Kt$ zE_V?=Ar+k<8+HF`2x^lG!VW$ym~&^!-R~0oJUB}d;7t8(Q{u<^PmhvpSR=qqPy(h~ zRrAYwiI`11FnN87l?OtGtvw5G=Pz8)_u&7!7GKUhw0v2wdU zT94@yyf70V)Xfy%KtSwG+vr`?FPaqIV~)Zl{~}MyhHKC{HLyTwP3vlDpGH;=aob&a zcWM(td?+y=LVb+nkekdtk1E|{OWUFeih5eog+(0bVC&yxt3^8`tO>Q=U_dF}yWsm<@QM3JfhQ2!1sV}3Ueh#-g$K}-;|y8)bg*^7 z@X^=#haVK&f!&&v%_b0j8|&VVyitwZ#{V`8fv(@=t>B|}lN-`2mk=8FCgrVb8`|gI zUoK{*mSiNX)*p)#IpnW~9igbKBPB6iueS`#^CPE2bZ71L&0M6@r;$UoF)D?9%1l`a z;FhHTeFvIm7d>3FTfH`A|RSu98$wo^v3VREnjPLS5joRVXD>Cvew_ywV20 zD$b}1dK1Kq?S6*!Y9S#fV#&QEK3<+TE6LE&x3MTg@ZDo~b`!rD**fSE?L0{g^nFgt zKcz>Rav5|Xu%80jaxK*MN+)wqF&w9~pq2MGh*&b4woX_WR~hRZmfS!@CU{Z#gy+BV ziP!&~`6L3TcJ89`$&dB_%qOxyngWX0kXLG!6^^|Ac3e?PYkOx zZ>`yxmmeIr_cjv?H!Szv4x0RE!|HJr2DiPD3Y%1&-IJpn%F@TF?GCo%b>UZR1EQ$~ zjE#dz^YN6|Rg0WbdnBymvkC|sM7QE!vuh*aAR2`Vxw9OWB5cfWaP<a_@CKSm-> z(_r{Sdb2GzBf$Wj@uQ^fle)WXx8^nHklD8s+NDt%JpX+E;lajnTwElXS~>iFGyT&p z&)9)(xvphyOP@K@Cq$wbhGh~Nk-;+O$)kcX8?acp`234px4AjJ2HT{=D|)IAvCDc? zrHFNv!7~6hYj<2Ru?#$V<<@bBS`=$uEB4D_#8=Px&+iNtSVJ)3`NGF0{tZ>ddh!ER z&A<7*wp6fl73xFO`*U4RzwMg>kcxK;e1Cg0@qm{vSpSbmIw|BJj;@--r6|D@{k$x5 zMdUX`A0d^XYPIfA1%G$be`FHZeDvvIt9GgtXZ=T65+R6toQQ;^lSQR_yuho zr?t1Ww4CO?j|e$mdjI)@mfM!J{qAu>k}s8yfbw+8+m!*%HaviZpxve=Qs=xyfb2bo z%+n@?t?UYwSM)gOP>rF{Akdb!Q3q*7)^FUDj;ezdQ#G^YJ%VByj-k*z0p>a`_AAOL zqI30b<_d%V4A>MkaAp7PMZ@zGNeq{@1{clA>0+K&G`kz#ZwnQq#cev4nj(iJ+d(A7 zBYU)IUu=q{Vo>+^+t?QVgv{dVkPx>R??9M0N@#K@SxkF7w2$?XmG$$nUB)oa-6wUz z5gFU=6?YR{W51(n3Di=X2;k{sLdR|1J~Iiss{Xi{YD+Tp zfcX;~e((zvyGO~}vC2aHj;iauv^@NyU;$gAYGy5HH z{2i1YEkh4nDLCFo-Sq3 zumE@f=$GB2n@MNY%^TGfVJhRo6I9WJQBjw}o^Rlni{p)-dp_}o_)0l?x6=~~jP8P> zCtj$lAIyjk(#k(z3|_|YAv@~3QcKoyZ0ogv07|Au^`XS7Eyt4-uMtx_$N3nGyR3PJ zl_94qS>}sD?^nHim|Q}Z(3ccuT`)lOO@rXvYUs+-bB~V(7@8r(%SBy zw#A=Sw->0o#}pZ0U(HbrDMQODMlk+abhpCKyMm(IxF6AwTK9Rk3ZrIOY$WdLSCOOv zf)a&ebyeQJ@OC@A5UapUw{;R!q?{4r0wy zj8IKjy!5b>c49uv-wg}P?Q&61dD?!iwdG9s(EKyliH9K9$b;X%DY?(=#%Z`%-jM~i z&NyGMZLUz6mZ{0!ITFJ}7i)}kmRNme(PbpfZHfczS&2nOHFsG#tc|9QQX<#Wkx6m! zWv&&JDLn~W4Eb<9M0Rl8*xF-hxO`LgYsdf}}eo zzDAGL1tP^i2P#Q0K#yyU&Cs|~TR&O2N;6QlAT5E5Q>GsqOf!vC7PXeb-~NL2#^qa= za?2KQKdoqZ!q!W=hPZjJHXM?>Ty5EPLgr?so;MOZ>nfUqk0ulicJ?BWZLq@*cBWSU zjL`WL*@iqBBk?GO$4%Hqsi9X!ZkS_3AWO>15c{gdn@d);YQu`fI@KecJg_}6@U*K4tbYf5e+z8?TL1Y}l%fzY0{T12E_#nuCI9@wzzlDdh5PjPp6=Gx1+= zp;7c=Fwzi&QCLy$-}7-r`&7A>A<7^=mm4>N%t|38hsSvnI!%qONEh7mXWL@Hz42u- z*p)Hx{Vm+Z!{Pvh$Ob1U;6L6okVN!P{}h^%Ka7@C)GtL5Xg?!cA4h8;Qy*?x!X^3e z-Dtk?F7ispbxYS^6&apM#$bsm!^)9BTOuWzm(h{f(FCMyeD)QY*NHe>*M~{PjIk&` z*_NZ+^FaNsx$ldtc&%o1Z=_wf$77xNGGXNcGLdx-EAJt{KY=5CrlVOJ3EA-V(#Dn zxcZ`@VwY;~W`q5gRd%Z>ed~+ly5`z z*@3^_NjOn>RIhFM!Hm6s(t=e7K2mH%tfKbvEsvOYiuDS|yS@voA6AVuBUx}dhPl4p zIOF+%!a(>Q5?hcB{a?!6f8BDtEV}kCR{H08gs0*4697|-dRg!hbWF;7^@=6o9K?;8 z0+$+i7)p_*Kx55iPYnG-ZFxWhZoh0rFqXnY!fV{ZhuJyAhEb8}j&qFScV(ugv|fe} zg*SdRyxSh+7ue*Ydi!O)=&@>3TYURlD_6vk_WI#0#2_yy&C9s>mqB2KE0W(a24pZA zaz5x{Qk#(tb&qd3guuA1wp;EU+}Vk4k1C$Og53G?AiVze-aoe^Kkm9v2qXrX>-8!h zi(WW4qTkh0Ou6-PL7+&X%u3%#PErPWOG8QfWeUHtM=ta62Y#cOYx-U*VA6UriPFsW z$3r8H(gUiIZrIo{wye3h%M@=I)b|MH>8r@PRJmHb7E6bf44dHRGMOXW?$=wiJ%1`UA&}0vg13cvAK)W4@KM`&#}9E zoax933`f;G2YY!Dt&1Of`HNNelUryR3Tjx$aKO%?PMU`}M0t`jxWcbhm{_)=d>2?I z#DrQP)F7#mIn>V&o;&bR+}i{-EW|g93LDh0=??Cd-FIl0%6V4uvmlm7!p3M*cPYQy zWvod^(b%y$NLr6CYzE3C#wA|)h~6mlINecI5=LnT&ZH7+0gE0e{5X#x67I`guySiH zqZ_({^T+afW> z2c*Z>(gws*K8RFa<|Iz^t5L`)&}@vofiKl1_8b~rx{(I~O~WS25-%9{(1%~JM0nXh zc_<)-ZOHvK?WUMyt_ytU>!-n_Eb2EIO}Y3-?r^wD-xRqe+>th#zid8yyolD*Jcv4w z$dxX>bk+~$GhONMMZt@_{{y@T|1#TjEhj2P6;QvU1jnN?TrrO)3B=T5c-|$P2noWJ z3gQaNd6Xg$JKc-$@Y#Te-NpvLy^U4);4OH@$JC;D*Y;Uf*ZGdR=9XGGS19-};RR$L z)V7KWLaTVq(NY4EnG5W~V5~-pA*xA?*96p2S9sT(yRxqSevG-)=QIS*eN0|l6ez@d z7G8uDZYNwj9^{)yt!9FZ!V(trp{>CoqXxQ&$lPt7d%;0$@pCdC!CSxxWoR$TBJSQQ z3XMV2b?tQ}e*SDLGbCUKafAEg!Z&J&hN3$|i5gKKo6jxmd&71u>rSN@S=Xt|CR6}Ii@t*MZt@_|1-Rp_WvBb7%0O!v;;0Ib#PhzExgG4vyT=2 zi$q-|l_IDdF=#+i&4^{8MviBRU^-Si$q61l91k#K43p-o>bH54LV8uSq_eCY^M%dIkaFk`LyJ_L!)*$b5kUAB`IZXrUVmoz5sq|pV`m(tM7;f* zO*KX<(v44<`%t=w3IgiqhDAE(`tMy7uaTGKColNt1L#j)Bjf>uf=2;k(W<94T=>MG zdCG+9dyP2^vTiWPyLdVQ3yekh4p6%dt9QKZVArx3AaLJo$<2G_7uN#p9&c}EzxY}$ z_kc1pf&h_rd4lGk=FR7L)1Gf@;%hs+)v00s$!q+!|5@)+^D0#4d*gHD*F`ckxl#~K zd*J(9ZuCEUaGJCKZtG%t&tp|PQ}&99e?a!4m=9z!nlf@`K8Or-@`Wr0x& zHdk5n!f1N9*>WgBr!+qWp@g?N(WOV0@0IM`_R!^g4KYN9TVZs_vr6e3Dv2$i&6AJ^ z=EBEO5KRAMM>`Iz=tIySXi(V8qmD?LJ5KwF3tlep3J&oCT(vh07};b$>n_|v^#b*} zZ`17W3+pn{1_#KC+=EuD88EdztYI%JL z@EYm;1n*NbD+zyf(u9Knyhi@}Ea;VlWSUw+>>|9?O@dIgGH)VX7Q(nZu#=$4{i;91 zm*lLfY>=Tnv7iXn8=XP7o!#Bq9Or(GmfF$jq+}4!6PA~ckwNY2L|a{AvO)OL%cD7{ zXh1acN=n~|p{Q0&mQRRA02QM4qm&KYf=AC2>EHNk_S0<1q$(?9%3bHw7QF`FpllNO zE_oHVAW&tR1)E>|z;E{8j0N$K|Fy7-BGU8y;Di0lC4b_CBPM_W>lfH_C#_XU5#v~(6aK71 zYHhPhC7YnWt4%-+$TvCI>)x~ae_Ag{uwp6wosMmktsRB^=jE2RkhCXB@@iv?t^-?Jm~o z2YGirfU|&v0Br#tGmTvVO%B_?Iu}M&F$_O=Nvd_KO!#V@*t2`=4}<5ZggTw6cv^JZXan2 zuZyaU_)cqspPBX#r-B`Q)1(3}UT`TdlcN;@yLU;`d-z)oCwA}l6ZSw`&Zf3WKh8P{ z<~mYg54AJm4#_4}1_Lq5r_UqL()Sc6JO#j`jlw{NKx1??A;_0GC+ ze4@h&??^EJV5&XHPNr3vaEqfYlwL%=hqqPKt+geAAhYe$_cO5hL)wpzhd_2wzJS_@ z??-Lq_)l#_N+WkAt=)_rO$!&18v!Gvl)aJ0R%!xL9NFt*)ZB}44By;&d*7kZ!zL+v zEp)k+3n(3RAKFz*l!Kdx=L@mhu%jEy@fCWvG-}Brkp@ZPiaW)^@=-_D(gp|s=+y5_=4>Dz9@N00o0Fyb>k zK7n@zx9xL2%1qVzq?^T9>RVXzF12`*cov@5z631#zisw=?2edEt4Y>)cSay@DgQNt z)LFts^_*nyMZI(R{)=#-Uhz2O0g)_dGZggdmN9b=lxeMEecR*qrHpIlc*lWWZN3<` z&Y;ekB$LNLvA$@Bhpchmfg?DbK!DA~%|2{OogZ#~T!6+6ixO$8An2ar?c>c)0<1f^ z2?Jh9L~#AdB2+1tR0Uhb1iQTx4Uyh{X{M1YhQcuxEQc!JdbR_R@1P`@Uq}3MRt3qr z5x>*`0tcc_{`;%x2jk@QOeu#7JqGG7VB8KwfGei6N*yf(cQ*!|1=)*zU(-XI6ptQn z_%_pyfB97ZW%VeveYf!-Jc7A@kShkOShwW+e(2Xs2^wDO#p^v9zXzbsKp)2qSyrqeh9n71sPXbfz0%WW!90CkcojTOVABgPVMxtMlrO zJe=uoy@GBp-XoinUIE_|YcETMLYJck!=&_Dqmpyw(8>#Vj6$&di)e=bTK>;4`G=R2 zqR_6=Tu6a~Ge(3=9hg^Z;6l^p>6F51%ibzb^_VBr#aLF+cavg5Cf*mlYSP*X{>^Su z4)UVzb>4kn?@0 zzhF^QQc<61w06o6ub-&Mizpq=fXb&c=7=kNujh*+u`&sQ^7Qnsz4oIQQt#PE3Dj$C z36M8KNxt?cD~D$$F!$mT$T|I=G(_12T+g-ci@RBr4sHqk(EkMk{h#>{{a^PSf8yqi zB_r{of^6qjhvJv1aiL7jVi^llg9XaBxe(ccYQn=t`FLFB$NlF77;yLBj=;Qp>3dvp zd<8J|yK>(^pzrtfr}jZjRKPw+7s{{wk#vdys>vO12WX4*P>B9-73Y8^9`>C6j~RdL z0CFW?P-L76ApfVxV8g)!k--|6$CQ_UBNWgin10YCU_C}%=ZONT{33It15>|r9_Zoj zXESS(YTh7s9xi)hw>Tm4mCP-04+AE(*7~uoW$>qlqfa)1CU!?*!KDZ5M$e=GiZc6J z6guSNQ=l-f2UPQCZ&whB!nC9YGNlj;j&O{zYVQgO&{ifruuCHup34p8m9h~j1+n2Q z8_0ed8)s?eRMmH$UQleXv$E)!8Urx5kf%GkREe8^44?Q1k2DRtv%0r`>k%X-aQ~mG*`gyqkKcJJ#1va=V9CEp=qC7GO zO_8&g3q@@%+&2&#t$%;H{Idb4v7z-?78sO4c%cQ@lCfcp&m!~vyD`jQ&&;R;~p#( ze=W0g%ei0Yy#w$3{MxWX{Qc!R&+&2MeNmG>S8=hS<>ib7VG=)T_>&BhOo+_ZYEe(H z8uudnrKuhXWf}#NNKre%Y?9aY;QnEOT+L)!(PZ`X zip~6hqp0xL3^{J&{j=@HTeUOKpMqU}4ZgorGk{(GSr7bCQJY;G>dycbwGak6AOq5$ zvzi*=aLk4DG2G67_{4EHnKCiy}R$c+9I+ZXTs5WZWqbDVG?8qW`x453^Zp+8cFJP>s|Vq zWu~gV=TQ469|Y*J-&A;t74|&Lq-8Cry~Z%NYqW}Q4OlnSKfz#cxm&ijCi~s9`Ev(Q z<>Nv_RQWvt;(}-ZkNoTfw6l3Ia_zCmDLN-P7d`O-JU5xA7GuhsW$=7N{y7f%63hDI z8nApMaI*fAgZ^{*r{_vG6r>d($)C>5L&b3P$%#UyxKqtb06%~&kM;z*mtcDGif>WX z>~X>UW1brh#+_LKHr_TYS5YZU_|ZRlJ9W6NYcZ3L7%hdn_H))*cv?_Qc!NU7!s$!i z_RR79UA$ccM=M(SwD$4xd}NA12{;G`9q93qz4d&M)8$3Q`Cj0nA@)8IeX4UPVM9CT zfyMt^XIb!otw3uz#v~-?MZwb}g8M~B>X^x_R2_1;xKn!gih9l9>38;G?tEZ~9(`c} zaz;M~nP>%ZkK(^I)LLzj-FqVlK@0HoyUNwnv~0@&k=l(=l=YEyob~fBgd6m2&4-1!mzbIL+*iy97;4N3U*o+{sX)3{s4B2G%bP+c!8+v+j#EPKBlu zZ}gpNQfc0T*L?ToB+n{1Gj6Y0DEeK>4K(2VQVlc;<4`*hug=BCiu3IE3xZrge4MQQ zM|_0+2Dvbh5v{k|rrTad#E`+BW6jF%G)d^?B*mI*m|yVvp?2YUXY^$VRZfe1&B8bZ zYQ8g#saR{NdTr)%V-2fSSGs+q!|{m74w8CI=gvI$=9=4gPrL0;0jw5>U*z=~_^zz? zAPLFZeQFVKp8$LSV3I|L&Ta zlqMDSwWxnl&v8DJ>ASc@bEX|#1o|M5DGy-6o}5;Ge;Gu9%0k)#NwGlWyh>RQ{Ok~T z+~0WYL8d(LN{;aDuqx z>S9tsF7P^lxWIzUnimMBqIHryIpJBu(gU6~G6^^T5Sgd?y_64*-ro|hoeLyXVPl$-^;N zs#z!k`T~RqoX5VDIhABnJU8aQ5%?f3 zWCd8}Y(-ah1@DIfRiRXAlYERuuk+J1QP#7>d>)_YjgT&4>fd88;H;hQj`HI!2qOJ+EU~c;YJVWo(3M=X;n#LcBY2t~h00>wI=y;AbT~CK(< zk_7MPe>r>wa}YXJ!a#;2=JceR5GEK0e6%6twC-ygQw$nQ4$s*8D57(49<1CQ3n?K0 z^DTJ}R5p`{)3?^Wi=j_Otk5R$(Hw8?3$UvXzBg+DK9WVlzl~Tih=*oBhSc1bT4Ck# zy(i6@57&=l487Nxlh2n{-xn)oAlMArg*=~stp7)Rh5qBb!@2^a$bhep;>&{8c6p~o zaq~6*78P)4(q%1L?#cEzte-9LU@h}C2(@9wM@3v>B?=FCYQC;=w1jhsV&LWUvZ2=W z0=R72iPGp}&>rI7f5-j(xDIxNys~dMH?-V>Xr7TmLK2Bg=8I1JK|4@Y36K!Jq%6*b z&Mxc<8oMD2o4rQ@PpR$JYmXj@rj$6FhRhr!l^8rmv(3@0Dqh;ew{-YxaWkRd#B?SGVW~!M(L*}B~!^I z4Q|(yL4S$Ks;N;XlFT{>v&V}+WuBG-kngHt91cFU2DxsQjermbos zTy>s)M#`4&*AA901VZI6*$lsJ!D*~${UgCExp%Wv+RbhP!FHkm8~%kbB8{wuqM-26 zX5}DfkT~Cs_X)!)&u#4M_D2NE#d!}yd{Gkf@$$s_cG;Y5+tn`#&Gp1?@VDjReb*vC z@}`#Dk-DFS?#1l=lkO99b<6vy~kX40okeaSdC&-61vxpZ* zROIPmZi0q4*}b*4G!KHWM0-)9G*(}@rm;MUa73u#93Qpbb2*B2>X^K7pE$I`_W8B0 zmK}}mmTJLUuRk4~XI(XEO~g6r&9^r>59hCa{Iqq-*E))?%CZ;Ky1< zxekkS{LK#@QkLZ)fS^GD{iUAj>~5S!5uin*LkbIs*B5KGEyy_Yq7GbWfplqC9yw7G z%zIX_@{T$dj**Ouwc1U(kA5_BZh0u)$+++wXIkOQ5AFQ4_o4a_!=LJKntmZ~OxvtADv+%!1!THE+cTE+ZzY>0EJRv|FykmTiI-f^SA!7P}7Qv1$5C6XI7mXdMJh_cisOT|0W-jrM zl&(bPa{NQ!X#=?8C~wOfN7vLmzh=h!T+aj=77{$)vl=OPMM=aNup5y6r1Hh@%4rY* zt-;SnbACjw1Og46o4zu_&L!0rikA}u1GgxwurTH(;}^Utb%54j<6Sqt?1Q%~V!2lF zce*ul-+o*-s$tkR=B2*>Zs|Otg@QbVz40Lj^p0kLC5oBjsE$c9Beyff+wW8+{|+lY=lR zj+R6Gwhia!wX6hcC?XqphY19rR7u1URB*3?y$5g&Ec!jl4aRb^TijcQG-LUJ=ZevX zDIb+CsupKE%cJ=R(@`m588zj+dy4s$aN((_YOaG`tu`FyyIJCoKwj|_|$_e{vi3B(KDh)K?deML-Qq&C5iz=K2 z;I;_qz}dkn0ksYoxOi%bz!7O-7FTy<7dRq4bi45^72D_8!c0)m9f%_mzc&kTM6wP#+;S8~VyBaKG$5-c6z6NCGJ=gO7B2fq!G6hh3kKqT?-1@mc~C#2OT z%Z^_VkLKt4e?-zBJQ|-L8iVVi@Yp4&PN=FJzgw8hXgf}oqRr7XjdJ3)$Q_tmVTPJN zq^)!n{IuQ^Jy>q(26!|SH^KOj?fR?DZHZ0CMw_#NGe}3M!KJP1Mw-kp!3#D9X-~yI z`xfE7Xt2>e>DVQIty^kN(L{#%$f~;a!Ns+RjM0p5@ATaJICQ5^Jv6|D zQ$4G?k|43v8KG4iBWD7(3LpH{&i3CG_{)ft(W9=# z7Zj94MMg7q;hCY-JLwII$pW*0&BhVmJw>I5TJ*Of6eL4oA{{mozXG4+mY+V!lMtU| zBEae##suWwJILhB^bI16JWgJkvFx(Xvjh4SDBi%=Tk`E~3RjJ+cC6le5&BK?7{p?l`3sS~a zqM4D|fbhFqW}ujLnh^PES}b&Wzvvw&1v3lwG+JrJ-cjaeZyZb7w5x4m6?AkRwzRL7 z6PQu}SXxI3gZ@nOu+`N!f@^cS}K)V30}zrVR^7$I3!tcRAzXwy2TZ zu+S&5%*(kM!pfTFYWQt+g+$P%_ZHt%H`s9L7fuOn1=*AuL&L^B@mvUT;CAGBrhkZi ziDKW>;$8Pq2+nr_!J4~z z$G;PMTl>w|tR7^youSxU)J^Tu$w3wLuQ(>ocoR1WI?${KNAiVpbPGsPPO=KrFDVL~*@+aTLa=JI2^^Os5#_MhuN4V>sF0>Fsv#DzqF#cVYe?oc-9F+A9*F$Z=Aqno*paFf$f&WrxJ8pL?FDjuZ_0LJ@b!lX`d$1zv}w=jBdrYXeuq5iF6JZ|+S&a! z$4xop>`7V%CkZhKEy1!lrC_aurPtG%1Hp&Sz1bbIbiGN5WzhI`6Ja2foTHX+b6Xr# z?gH<*je0Kl#gF7bAL>d8F@Lfg7?*|&N8#vy@u(GrcgeI^AOC+6byd86Sfc-VD>m~o zk@h05RQdUq%43la>Wg3lAQLwK{wSSp!0AzPd?ElGLr;!UB9=sJgv?@yvT~A6nlK6t zyA^pjG5<1r;Bj}RgoK)oi}TcC_vQ>bZybikv`Cepvnf9`O`Mu0zL=5lCknSPFN|+E zME0BC=;xD3Qr-Q4sK2deLF*{-*!8-&v+z>IMlr)u`H{!ChNRq6k{j&3PyG*COlU@= zN7$~*E_@+z4O5im98{*Ub$PKRh4;usJ{f1KOMo~HmBsMh%RuV^rx>^}>tGYE@jJ86 zOp-?6OMO&mT$PsmJytSZF|H^y$T{QP%)Y5g;IF@8eZ1+^#Im#Lxx-=WyAAj__x~iy zrjF}XUKAgP{Xei9wrl?f?1m(tjA5K@XNa)lEx>ND248jOB-{fHmWJ*mK9`2K-&ehh zIT&yyn3 zc**O5BKh)ArV7sn#30YZQ%-;+I+xoz(+A?L1nyuvg`3hPGA6{KC+4#?hF1o9t1$NN z??oj`1X8!X7t#CjgOAf;i%S)5_pB7c$ARAP3Qq_G8oF%QCwTX*ykls{t6(|=g9_(~f0KEHWknz>ZGrD^iGM$q|09roLvA=xej+zFMB$Ntna`&GG@sQy zQ9#YY%^KxG2?GTb8r1ud`t2)d7@8ao!*e~Hb(OasU_5{Q+1+JW>syEBSAg8igz9{D zMGVgWT+UUF10`n1%hubH2Iv4tQOKut17Us}1uc-wP+qjm4x;`esqh zXsiax)4D-X)Imw6Hcak34?jr-qh$r`$s>@h{}#Am{{h^9#`jNW=aPF$<%naEt19#C z)RmuT&?a%D#3MtECgU)UN$EshV2Jg89S(XV3byrW7>*-u|0 zpIsixGaCZvN-PVy4yr8)Sc5sQhvT9}(Jzy|amJ}dc7AN%NIQe{c$-iWuKYvgLt)zI zv36L^XNPa*Fz1+D6z_)pyeR4)eDvgy(ZHRQU{8kVpO)vy{>i)Hhg)>(z?Y#LzSq$n zz|_S)yBcCiYl{N`NUintou=;gg4+Dxxu1ky(+Ks;yvSpxuRzn#-(}X)jpDod{?|xk z0O2rtD?O})k>d8!g?+(QsAV})4~lc89#uZ%bF_6ri~&K&8QTNjUuvSjbMK5M>Yw6V zr%?Z=2d#vPCM@2m@#8N=L*%;eXXXrkzq~x=3>}eT*@(CIH_A!U+&}~TO?~O@P53QO z5^@0{oYw8sYx84TbH^JPGod|rT|A7%4AtFb(GcUu_#h*^E@a@H?=jmpLXg&$-$O*g zk;bMd#hT1MA@QjSt}AV;a05pa_TyZxz}6iI(35UK;2t=2l@^UC^X)mAAx~86)r&%H z*w5QS{M)NA3@xF~KuBx63?c1!%192zem~&gqY#hn)QC5b`j0vYGX&iQkHS0msvfUo z&;mC;Z7nK(^K4)Vn(c5`N{nKPOWruV_n6-uUB-9Am_Vv4i^8j1;hFX>t=-9#9AK@No$dE&&NAQCP0 zXX>EZA|sP@z>Vnn^4+9dw!i&b^W{ELQF$ifuWa?zB43Znkqd9X2r&x|&JQkkEW`B) zHTcvhiVm5Whg*o9NO)CNX&KWj%;V0$qB?D5?ILgFj3vs7(5%rKLm-l72bTusHJp1i z%1D_a`V6jIhe^UB2H!Pb(Q19q&UBS6{r(_+gso!{viV@mPx+-Jj2E)LNffK7&d2i<1~c+ zVAxbb7&dbTCkz`Ol5D3m2*ZZ=2gBxCB{3P%ut>EaFL#nRj*P@dNi2WG<*r7e)vp|8 z(Bv>Y`yCdEHIgfDF848mAR=?eb?UnLy}|~$P-IE^h&izI!-u{vWsn2|k1e3a`55@l zeDlYKFl_Gjn3LG0QK(84RziZv5ov|3d8bsLL(*6+@KsC;^E@D87pHD9O)0S1cF~KS z8>i=~S$QuKoUn_ae1c6n%dq*4$(IA^Blw;&`5*&2%oMbMF=L4j<-{jah+>Lc6!;Q> zSOLC7?QP)d!Oflt2)UlafTj4QqGv2=FOp#T3d3}yLJ)NT_woeWfoJF5YtFcbDzI)G z;p`HpBU_`%!Nqg$lIhkg(F;K2lTOiu!j%C>5gC^Az6mOVznr%J^2t#a6H-A)Y6W#w{TONRn?ax$)8@d@>!M-0weyK?`!*c z9gB5{z}9DaDGBCQfR3xmSKfD{ zmx!NB@;n#0MX!AcIR=rT;i}Hf5o2zzErXZNh)e=CA8-q|7w!~Ak1@XATM>6f%@?RS zzW-oiWh|y*ZMxG?aCh8nDMat!rtH}f(VyIch%XA(xt@7|bpXtpV{m=uFEE;__V zLsl{ue``8S-p|mbxz?pmgx`8ihTXXOnS3PmdI@J|t5`nZV%60Mk>zWN?7V-z@YGMD z$%ThHosX_ToggXBh)axaCL^ZxUBwdwlSY9ad*UZx3{WXd-*zWy50GT43YI~J%C9~q zOV|%al9d-f2SnhY&dy&H48!psFwDt8`Y#F%1cs50cU?s>G!Vw3%UMwcPBhFACz{3X z#M=7?z=>w`#EE8u@~0DxJbD5>1k{@cPBi~C-F`nu@R)nIfox6#s9CiB6^4m_uZ(7&H!E;(#M1*c9j*^F!kj+{gPsIqqN3 zi3XVgG(U8Mmq&8%v4GO)3?Qq|-Svydbad#}2+dh{$!K;H=6lgzVhm8j-<5IDYya9h zf>esQ8(K%Z;^-%&f6AcAJ7LhI>TN2dMo+QJYH^$x(Y$e{k)!4{Mwzqk%@(sEb9xmt zREb4}_sm(rB+x4JwbMR9UW8S|PnXWtuc4VO_y%+T>e4y1U!4c3k#@lMxAiv1k9r#n z6@zFG{iZV);ZU>@5~U~-S$h-Mt~|-FL|`iZ8VAIOK5bd1hg~v9K9V~s33xutP0GVa zP;x`#&KlXpUOHVP_$xZ^$lH>cVd!i~RL6GxHC988_=%VjF~@4vIf}VmhL_86e4b=L z+EIpJs2GD>fsMWeRvN8(imXj&P#Z4_Y(GQWBbZI|LZgAX17vF4`G+pgFb(6R91h+p z`>kC4gjN^06qt7SR7UR{cZ7?R@(7YnAi7Ku@cplupIpD?lAm7E`1(mHZY5sudARc8{5& z65Hb>?q+eXeC4{wU0unFT7Dn4bt*ia?8>uKjbY|VEd=Hx%!9WIR160>f3E%8QReT5 z4Bin=gc%bVc|vC;mOS=|L0>>ARF4RR2YJewq5hGI`Rv^syh58x|rusBc zPYhW%dHbwhZ{lMfz15AAi~oXpjudm{ib@$VqP~(eVr*cnAe)FYRV%E}4gz)yA{Bmd zqjkgRDM=xgTfq?x4&MflbeX zVb&^HFgEFEV-aa9ILF=z^_x4sA96QY_4e+|_SP#8?Bi?H+`fIyU9Z?#fBRH??1Kl{$G%=i4L^){MQn_RwC%Av?ZT5uF6{=G>u@i|bwjbHj3t0Gjy! zN84LQb(wYl!$@}{ozjic-QC@t(%mR6jdYiUbcb|zcZUK>3zCBHTL`=U(1=_ zi|^8H16^%pU}5)uMK8M1M5vO?YN7i^_O`x_(sscbDZ_fE32(&;-aCb?az%ijr> z1-5|AC)N2K9%U2o*x$Uql*?9cyxQFp4=5y{klYh2y}O0~Bv^t#6mUdu^L>BpR|j=K zN^^ypt#0#@%-W3Twuol{^?qRMbMB$>{+?V}4gCI55&db+s1hZ0|GEUQlXW%76fJ1X zWqpK2mS2J~*o#jw+lRz5%i>UXzc6#9QPJ0!_2N1=zd`8BK`OJ>caErVbbdrb+4*!4 ziXZx5{$r2P5+ZYp-OCv+VFhWfQ+MqqnPG+?5JG5~?JZD=Oy_h?{Ql)OhZRFs9|d_dJ@02w6+st-0SefSu8>nd#vE;Uu-bnQB}^~x95MH zT7T#Gc%NDVB?S_+0{=PcNm$$;^<-$EJ>8yNZ_6-niDGhNSmrnF(P9{XP+jgu51|VB zIKQX_t+ac`p=eJ|T;*hMRXwUx?H>CWlic>Ji1>V}f!*zE)E|c(RN&1iO^PV-KC=+? z)gL`a!gGbH12T&_%l%E3nghzGhfIWRtjoy~;De8V-#^N%KfTy^fG2|QUTjjM24Fmt zfa#~nz3FFgp|gw4p7sUttGg8^KjMH37`c<638&RY$QZKD5miW3MjwoL;9!H>7{DjYOPX)ie9(R(0K1dx}T2^gl=ms-Q#Qw8q@HlDTtzI28`T=(K=_*oO| zlsN)^mZd1L3P=nu593!U3i`kC38nQy4>bG$QQ+)+dNEt_B-b~hZz*Ho>Z?4 zGdL`(78~D|RM(ZJ#Gr%WXMn&&`6dPP_g}R8l(@7HWIbj}(441T;^#3bpB^whrr~kjPM)t2`jYgVl+R#sJ^?-8j z4;7OU@XA$%I(^QiRe4d-w0Cln!C8dBG@Y3Icy2s7{Xz*HKr1c90+`tV`ke!&E3Uph z-+>(i)+dM_c%@COP41n0nCHMn_U9dr(WT=>Um zrJuh4&#^}3z3oWQ+SY6azY1HD4=%b$@*<_=nktbwZGP4OGoz~|V8#UP0f!na#(j0Bm9MGx;ppt0O6n)g%9csp`K%T|Q9^we2|?LnE=7ixrP4K>!fZy0K+6+NUM`1!#dA<%@so)DJ>f{B-W`Q7weoD z2MF)tkK4!IPdl2yPfmWA9s|=3nNiceljFo*ikZabvhn1ezxV`d$ge!v1fube9d7<6 z8b4CaIYK}#;Csu_PH*q%#$vApTS($5Rr`<~9qODN_;{YB7tlHMA0S@@gbvwkx4kEx z8##0+*^ixWdq_bnY#um|N`E~qO|@p0v8|30mZqYutQ{jBe)Y{E=Y3!mZJ%7_i09lU z;BSKl_}e_&$Bp*K7W>GB9RH`k4fXP)M*P*lV9mnp)%Lk&Wl1^}4NxS@9#?2tPeuTi zI0I?a1D3Uvb>3M}N^Bm^LRc{nl|wpr0|NNl1bz3n+4i zPmTN!f19{!dHH|kefWD!|Eg>DUtI>j`yo9S0QSd(vKP0c^<^!TK>v=>6=~SuI&6Yk ziTkpMbMHaPLpdn!{^n4irg4t^RV?)V5Uo-6pafy!(nW5;7kD3T-ir~aAGqW4iINtL ztj4jAo640T{0X6(w#}dN0+9a(Oy58$XWLzB4bIMA3{|z?#*F=COs`=U&vygl)IXLr z`}zBSB$G~*+`X(>Pa7a>2D8_zKOo^ENckohu@})OnA!Q6S%}j6o4wxF>rE`!h~tfx zf=uH*Owk#Q6`WTD$g+*9$i7vgBi?r7d92>@8vGRp;~2Yus9Bj_OW-FkKcY>`CX$jf z;^u<`Ixmu>W)1lS86RFtr7HiV%XQKa@&V0JR4LXi!&XvODuq~1BSk#a9O4k8x`m{5 ze1A^++QC$@x%eQ^3mbFUIHTUTsg!2Em;!*P8EI65!8?@xE~OjO785F(5U!kbISSb| zZB4l8lCuxP4uMB6u2l?+XmzourA1rXB0LdNQo_mCkwPFgj-{ny+WoK2h!7BHT4bZoqyfbW^ z?}#QvOLM1vPy1ceYup#qU$w#nV^%h--{%z?bKn`xjVsO>5mF-&@>=i}ccz)L6s}Lk*HS9pMcCe&Z7p z3PtMI-Gx$VEu?pP?itC=1gZc=?t}R^nV*KUvW`R}|9Rx_L~;GDX$A~ut$jTP)c!J> zwve5PdJmM9IB-z^N6@DKe*KT4`gu6(!pQ%Ihr@=Mp6HRE#rNT?FDtgpl+3RdO2$F@ zE&k6VE2`6dVWsW1nIAXJJNJkI$cqmZEK#dblixmo%+R2VP_e-;RaGBt-3>6c=Bj3p@4%DeZjqhQl4ZBO`xhr7`JoR zC0XbgJpPB;cG@luBlJu;UOKU=WLx`aN6^PvTK>lyZs{8JYb1#OHi1Km6IzVSj?ig^jQ}Vazf7Om5U?POpyuWQFaj<9(sST}=z_mOY1lMtxd_`z9@_V^JQZ?`mKkeJ_;}tq{tCVf8 zgB|ydw_IAxkj)64wQrf-vPvf$j}ic$(KDxHUS=7FU_PzSvg?hKi!RAUpbP;b#4l-$D+$hGgq2tU_1JH0gCDPC^HkFN7!;UhU% zE=G-*tDJ!9Kj7gxczd|xW$fUQv=ZP?AhuWbvv80*%;we)*S@ls&k?NDa1)~V7$6y& zODeu>(f&pE?0GDyValI}aHM=O)m?cB~mC0*$;>Q-o zikLSTtlj*?`r*KHZ;P zuwkK>AHJ8XI?|UAYHxRa<57p}lzg(%T*P}oQ_?Gl2zu+Ad|*UN(A~;J7eq(=0RhHB zf;Yo=OYFdq$B##7jOgT#?99FcRP1mH;>;!EQHQtDkx;#?1zO16mR%%sP@mbW$l{Ty zBVzynl}s3gvZVQ^06^v8tTl;{+~;2bM*fJ)4;Ab`53Rq@Wxk)??ulm+h{e{GSxwv3 zls^L_0gC-uWDLaAgi8?W)Az&HiVjD(YlZ&zc4~UPvsNvce#Zxb1Ceo|g2q;FMp^HH zkr&dk7u7fwb6xr?RLr1~fI6Kvka+)TR|5C z*T;TM*f|IURV>9Y-Px-h!BmbAvo~3@%Vg)eeV|uw^^0#zW!YgFG zaiC}XV>;_EG5BMPiR1z9%ipO~y!+Y4l?7880n)_2vU&cN=xX+~*h6~qCu@!l#L>_Q zV_$Y>BwjG46008(-SIcfr~2Ya+nM4|^(Y_c)9(Q4QJ<&eQ>!oDpl1*e-TB?KS+hrN z&M7>bxFIh^?NsWWA)1PgN-evyh%_1)CptTupR$sSkhg`WlkYmA7uZvW5OZ&q?6<;;;DZ=lQ>OX3VHwJW~;(jS~u4%!rAo@_SS3U8zBMQhS0!3K$L+9{>HNfzjyOvd*qrLN< z)%eQzvW3rn)hcI2^U)@R5v&I~CdI0$pE#b=eHh+lQ!3Ib)l$TpO+X;jK<_!#RO%Hi zjZ<(gl=i|CM3GCI>6zt%wh7BG++``n`m~{>KiJovWX9iX)?Olnrr!w#iUm#oC>i9< z*r{?G$YbAD@WB?%-YTW*0|NIARQ1C^jVDdQgsgteN6#Cq%L;uTN4B6oYI)-EaR(E7 zJi(l%bA2*hwL#Odz=0*km@XQ(!;y#&{{t2o2f56m$~n|!20?mt8hGTVL2fao4@1)| zNl>Oy9#VWggkL%Xw$~QOHn*GG+R{`HpM<6oZP>}2Xr{?rV#o_RimQh)3O?GsgSoNc z^X={;cO)c5xCQ%4DH5C(=zaG<5kuB6;JEyMWg(izHG*>3g0V8;+j`Ax zxMuD`g5aq-Tj9uP7QKlW7}>y*(bN9~w1$V1fN^>5g-}?nxfkS7wF$9}B8_6VZ=wvMwhrGPNB{FE8u402s_Xl;IW0JM(myF}b)}amF9FAc z@;z~{Iw&h{l}hgW7p^X7vG+ZOk#KDI^QhOj*23c8H2O$#MsZ>cNh7PZYEnLYhTj`9 za!cK%oaSfSlmi|0=qpOV4cb~OU{)nSs1Ti(aptPS(U9N-=qxR6;O@Y%! zrNXWw!ssbQDdF4lY4%hlqWH&OD`St06xEzU+Kj$|o)*rLx7dDY5sZ;_`GTNBxeB^8 zP~?ro(5uy{UG&BA#@H|-^_|1+<5;?IS4Vj7hc4Hjs+-y-fR=tQ*ed>et^Gl`e{=;v zy7Bov`eV&xENq1jr$Y8k1e$ce01HRJq!6R5odxZj!=m6$lPj4JsA00kh}R^8cdgMU zFS(v4Qni)ds7y<*-t@yS;qqWPMkYzIJhJ zDQ6&cjh|6C8;3ul{_GF3mZRyjX)4L0r9XuD_8lbN3r*!%77SrGBr50iQ3sfJ_03=z zvAXK3lwCLl;kj%j~{0o&leaYIx$I zUl*ktJ>LCQ^#zcnI{zqa&NfC)7PdCPrs!be=m`8x*1+22LE=Ws$@%jm_p}XVu*qxj zKo``*;Y`*b#1P<2fe8qgKy><9F#EUyNE&1o(N2Ty5!>5+PqT9O;=xA4Rln-K6EZV` zrk&!FCv@B_)@4r4=>%U$i&K}~?Kpebm7Ld1uXJ==yOG`*uJSnICHl;mmQCUx>P?x? zW>kkdtVnJ!S*KJ{QyOJO*}g>31C=5GC64l)7p2kfF#}D4p?|akR&U$k>I1b;?j_;H z@ww`%BkJ`A8WHvrZb7qjj2bFey%gISYD-v~NC zd?oYeg&MPWomhm@&9+NFY|ArtK8bLR;bO${_idjtz`)m6M)R%MIt9-hVbTWS~L{GnT!sW|4P)`&wA`v zoTvVEjjL(mch1Rl;Tv?mLPL$XH@QrN+zT=Xz+Tvmfmp`{1V{0(q zLBBFbCeMa1XsRs_MM^0Q;e+gCZ{0dI`m~rE zOtv}H{=Dd!!>6eZzhYPkIr$4CFCYJ7t8f;hgU>5iW7uPPXSa&zV3i1&Vt#ZLwcnJClnmhogv>msZ{ z!_I+cSlq_NlC(V$c6+&jXCex;TTPrk>C(r18Z8BwCj4$79utUAGiCpfI{}j@%OmGsuWM5YJR~{cZAVK|44!~zEk6O+wYDztRZD+mv!s= zWVrHGS~K8|CHHUis|R~X%+h_20`!*3*&vMC!OL!So*O}J=5K*FG9T)Oo^gCWj zPtSNfZ9WyYP`(Ca{kc$cpgT%lNRqbHF)F?w6H2`bYFziMKuX|fwbEn3P?FLUA}{f& zm~b1NR|9VV0&9y10X%HAkzH%13!lX6OQs?8UY`V!fk;)*(|)pwI7fYYo%VPdeHAe{ zed1tPEb78XJP^%O`;&TLmP33oFZR8@tpI&~D5^iogD>@)LKM<)x6~v(mv9czZQ2cKUYmc1QYt z>>+|vgvHhQX`T8e++?mLhq>0LR8bd;JTP=xNk8;ap-)2Tmou@=^t`|Fz-7;%YoFY>! zw>_EGta$;8RO70^^C31(W8oskr|pRc8jH)&rrszqV@68mLru#z3`H+OAh)uB-BIb^ z$Sq}eyT3bNGGg_%NtIjb_qzE`O(9}d4@1}o_qY*ZVCMJ-J$=gJ@%eP8 z{3!Edn6roxaKM~kF)QEl>>zRREK!zP{SZw5RArCE4Mwwz{n=TiLXVJ6y>NtCR}RHv zaN=+I3fVh)0aG~28_CtweDhGGNbhD%Y75Z7yuHAzE@9P=!NuKRb7f0?Su)}D=^Ma} zrMEx6XKMpJ(3gg_=Zp(-}< zunR{KF7YMH*boCT&CbF{{DIb!9qff?mYH5DUyE1wyKBH%no;OcaX#^`!jVcbOd3${ zRzuW{o|3pwsf#Gnv^d@68isIL`)jy8itLI$q&j~H2(gf@c{K;DgDS8l3jgkU{HS-6 zpRZ1tQoz+o#24AYZUs+phZu{6Qs(~Z6gTO5H0TSdi9EzMG+fg9X$6U@JF>_#Khf^9 z^2E(_?VCdaKF_t9%8MRe6s6FD>@y^$$la@%kRl4|P(?kQ#iu4OuG$QwPlu4By;TTe3#{iD_9iZh2xLMn5VU0DWk5E?m(-4={U81#yEQutx z72IIJnOK&6-ji8cN9wQpHtx^+gf=LRydWb;_K^gp0}N$9OCsr0WY> zDG8U(#n1LMs;{2G`N6a_GLSE_lN+(Q>#znj7D0t*U@5X8>fO*~y$~ zLj<;-^VS7oNJF*SK~7TH_jt+kISOV?87lcm*S8PGGVQ?*PAI@yDMI|V%|vOUbP_V#nlrt0;3yimEIKDUFz60?-?67HjT!m&7`^W@QBX8KMsQ z7(Pq!B9sYh7NmyJV}Quv&9~<}T+^zMVO3{4@`c z4YnA&EX2K@E*{f7_#&D`%6hd?v7nY)NCXXn?1T$CEEw}9O#T9ToQUiEEuqm%9cf9$ zJ`d51^!8nOy_gdneLg#762$}%DQ~EXS#16yLWB~Z6e3@iT^6k!mS>zK6}lkWDMS#< zq0QrQX8a+UhQ!sP1Ju0%^J$%!k)-TC_M*GOFAb%Wp=(j0EZZ4KINn7*x19g>a+s^y z$EWgGS4}wYX(S8SemGqsNGq4*gyIGG!9t)~5RKilONDadGAd!_x!0PHLQXRwZZ+9xKej2dW!~D z!MrZgFEoRQ7{aHD=4!;BVzx$h8qlI^!L9}0all;=eD;5_Xm-s9hrQCLhS$IOj& zDn`)~(f83kWHEKTu9f)E)*tV4N?V9!tM6wK$S#V_kp5&|NFA-@%gb_Y>hZ#o-!bI zC>eoT0Z?UL0b+-WXHT0K-1ox{T|(*IVrcTKn>1sLCGAliN%OI6lx*vXhj(s{UIVZ@ z%JMY?4)MDuU*rV^52CSoR@+7P!j#ucl%Rw47GpLQ1@l zaiEjX9Dvg|;24X1BK*ktlgowtbFCZe!y?IuvMOG)EpI`}4twF7o1>Hc!RtvKULscC z5lR)7z2fF|sI3?XQ#PGhmp2#(J_i|KU!vpB-UjnY+mNj#-%!hnCRSeu%hzNseYSl@FDl2X>KAKhH62|hoCin}SaeF`30P}6FwUeV_TDQe zO1Mvy61+kf?a;?IoAo*^@?tg3%6?Z*>tP~*JXzHOJN8BWvVh@TtNrZb2)1lMjY+m1 z!x{ec7{B5JNAwqT@hgJ)sF}y}C-ESvep39sq^mKnFJ6L9gSMc0q$_CL;mW|M$d-wZ z#}I0R^AQuz4JZpbNI9xTzuw6yTk$qQl}FXS-m z#DbV7>yFp$*e4g=fhFJ+LKkng`5T)Dnry_`Iutl~3227OSpN z!Mp4bKl+#*dQEupa2k2SjwaXTmr`Yw;A$!*Q&}J6TOAEe#HH3n?v?J05w*C2YMR(37MadGT*~MN~Z+>B^ z&K{5$a+y0%GuMvw2#{p_R!38pU_Dha5$uoHxCrb^y%OD`!*UlAc)c@@%|39U?CKP+ zYb%Rl>{YjeNy2qcnn?~D=odc4Y9z?GY6Q5fyxO-EHU840@i32c3!nR5=<7_B?ToF=BSa?LhwTleRrMaRCmk}58^3&ra=MIR923W~{ zsBBhd7d)i00@?QxEQ~XB9|a{oT1$(4F4Ix*U35-Mj=7W+oR>`+W$>_nu}N0s$tUCr zs!PXEK*QGnOg{`h;k7K+#`?9b6fmU=Zj)N|u&SV{+L|f1C^0$gSys-!%o*a0 zD0^Z4k-z)skZeLRNq*2VaVU24Pzvu$eys{sz7Z-{(^!vku1HILFM-YeHm{{s-NHxz zdevz2;;7!udKKsOB6^#iQwexTq1(k?_^39mpgRl1Qa5r1)4;VJbz^7iAH(9=_Yb_hC! z)Vv=_fni+uB3cZ>xyNV<&Le6iuKVHvkT2sc9i$SlSS&!@>HZu24ggmOTG$v{*qHs@ zHWZ_02{1!@+N3i4UG@#edDDeasPdfNFk*QkTT0E&zN*~ItUPK>6<%cAPl8^(;l5bj zYw@WZD`JtHv%2YD&^(}KHN9$`u%=LMQ9L6roGnttm&j3>#_mm?2(By1s67<-ylaRYqtbBM&CnKS$=|$v)e$!L8{xr9GM@U>31Y)5dYUA3e0+c#K zAVbi+e-JwC12UmEL?_|XcC$`1gY;Bnn$E$Z*9f5>NZY0KEpTnRjh{rDazmZy=d+=h z=ytE~rJhOL5?4S}afNok+MrA?72_Uv$gP%litLm#oq5ZP9_W($=iRuGb0lzCB-Wv% z;Z!3P6Wg?PI?8BYq-kd^FLn{;9u4)(ru_wQkrob(d(S0|2bNU(|78s zp_gB8X)NpBNt@57UKRNxy4S{2tr4zw(XL-%|cas}Os6}qu zCBvii0w-xR&I$w;J^V;|k7!!K6SyG_TOnt4^Y{lTK$8GW68598sH=HkmH_qwaWh>j zXtHU=r990ZX`jnTTMAd&-E<09N`FPc30=JA^J>OuzMFu4Al5T=3^MV81GTi74$1zN zesM|GyE=w`B4C1`+~`$dj%#avY_dQSexmbDRFb4vGyJa8+822FJ=sBA_SM-ROKsWMg$rzYmcaBks!<21>gIR}Z_u62 zlGRu{TU#Ft6IRP^$*O?GlLN-(ioaJQKY@~dUA!Na(W0bdJ0%Y&xLhneC&W^+$n;7t zft}*dTNDj+>mRfp$dYJa+qt}-nb%vygs8nO>MuKRjEtJn+y3N5Su{DU_i_P4FFiSR zUtX>C9gX0Gvd;+?e3E>v&)%wvO4peq!SIcLvUL6pf)Cf3&Btb%`cb()WEn=nj<_oU z3U=;IXPr3D(P6_I0cjM|fXg`X8y??;*ih0!7#n0;BC+L89D9gB5wE6xMo-dtBAa!B zN;2_x*fuEsIwjFQ`F0U*2;o8}d)DEb9b0op8!k&YorL?`8V zI1X}zd*6i$VNg{(XOF+^5Yy>1e|Z-;I`nk1c(decg#RQqzFGo|E1l;;j_qg#w<$Zg z-e2-4^89T^>(|7vGGcX}%uh)g+o@6hEF~HxwzWVmy zA`Fv~M^g)A#{DJWH~lkyx}*WSp^4jn4xrwnQi%RU$dKzz!zo#{VdO4>r{F8BI#}I7 znjO%si>HU_uj1oTiH7^e0+3FtY6s25qrfH4Vxs*f1Mw^b(jh=+ol2wCjPO=VY6DLp z<3!o5X>tS;Bx!=ZN$#*RS7AOiXT-Xde92e%>WjgMC(KCRAIHRM67Z~dtU>hfozNj@ zQ{cygsDj}+4AbIhZ~6r+4#XSXc_OUVs#Z5y#`J(wgF8ap7N|{PySB{L%X*QzjE(CD zq{J;)Fw`j^JQ+ZE1b(l8evOHBq(ImYy@Ishb0X!s3|+;Aq0kBf;RGIWEh&MiMW=d* z31NwCvh<7+`gqJf z#-m5<;KbC&YSniK8?MuB;ujC5NJ-`)bzwlHK!Ave{9eSYEDRZbjaqHQe}_pVZkBRF z=AP6da(-@at-pJ>f2T>n-THPBtTH~^4%o~)d4oFL7x>CT;P;#5^HYQU^`m(J?;_yXFLbxH-G$xgt|Zut zOleGw@8?Jr)QkAB23_*5MGOH0CnNeazrZ|%<24^s=`sB#RlbO>jlCFgeheYbX4)5n z5kAZlOouWiOoWI*YHF%itY=npG)gJ;Ym;`V7d-i?WPTxH`kMrw+(GjtDcjsrdVg1?!k`k#z*gbUFBvm0s{Nw!aO@$`qF#wUSFfQRFSHA)UP;7bx(HmbmCOH ze2zZqX4}YQu#$B~=GFUkvWPcWw~-Ho=iXQQH({0kQJ(oVQq~doa@qIy`7|iCXnL2F zm0T$j7eKdk>oMFRb(tjcw+qO;lW|6{qYEN#BE)>jYv7s_nUN}OU2d@Jj^lX>?vR!ZFK)8}$DC$mZ=!Zx~_hxoh z%wA0?mHVzd#DZq$r#?|9kj!Dm3gYj2q@=U z9Kq0%wL_`v=8rmdQwL7+W?9CTRt0Nc~BlRJH82^w8uPFCIELy3g~1p^5rBiYCe*xSuGH@*c$_dBgfwn|Hj~eI`w|H zUt5B9JJMKqA(1xe)NQZJ)|yu@=~DcO{pJ%j;bPWFX!vA!@aEND%g8*nC&U-%G^X(X zNL%seIL)Z6*)B37-|F2gqQxP~6U#-$PRKl`h?ulUId4~o#0~%~1%y>fF z++u}MbmP5t<<(|*C*NeOlzXLkrsw-LfgW{6ZRf=DWcgB;QXc5Fto(+onlUyhLfZ^!R(wOljqo>eP zd9lO^9#5W1eL)mYXH*}f1E(XcgQV(7MI3xbTn9Y9ei_Hd#?S+3MP-|X5?54T6H}_k z6{GJ2$4$pMLbAfvQv_(Lbd(75@s?#9mWQJQ$GV#YL>(CRNSI6C_R?sgLT1t48#Z)Qy&y#`mz)mn=Iz8; zAg+wEYu_R+3AbPzjCQk9j}+8}Ba1+So6mVY`y?kbOE^|xCZuxq!(alQ4^b6{V9;L4 zxxka8s;E+5cX31-;x}pmff$iS;;PQb1FsP5qG} z2Z0VSu;`(hSyUq$pTvFM7VRzG=uEk$zn5N!qDu9}z0b9TUt+bgKo(Zky>c9-^eG)$(sbNTq2 z?f%i+ll-|29sL@2xFj;3zGKVQe5l=z48LZ$-CDbt7f}88Y_Z>Ljej{h{AdXO^%)zL z0)r)pQL0#1K=wdIBC{bgjICHI0!U(oeqDI0@Xr{QHJG=-o5A;uh0s0t(MroVx%K77PUB^xjuwPw;&$v~DbC1W>wMXhPwvQ`6^m_X2! zx7&PVt_Sn*cm)5Yqv~FM&k5*szj+w^`ONTV=Lb|{8R4NmyY9?DhEsxO)a}5zu@FGZ z3iFC$5%pdkvXU`jk05?KShFF-ayEmT;zuP~4wDoD6>*%3y{G|HW5-RtT1m@YywDP- zUw7xlUovSOkuLt`UR-Y{Vm_G?{I+zejxHq)JXi#VTAPYkP@-jV2qF@^N8aWn-+0|v z8B4KhgETrRGvF0_2u1>P!ZQL`x(-IoKn~*f>FPPeacRn7#xinkSh3x&DoplsxwhM{ zrWcYQtr7m308UNQm>EED)`8z|G6Ei)6n@0V`ai0DWkE8Z;p&tcH<|~ULxVZ@-j*Dg z*Z)xMqYy$IyFMXhNQy&R)aX}*c3q+B=U+z7yo)fe0 zyS^`1cK0(l~e=g&BV@cR1AGW&Y7G9`1D@g$_&l!#%Wr=AW?mmitm6kMl zYJd3jcuofwGk*e60vT_g`RbTB1OyDAPe%}`@%Okt+U$wK%`$*fr}sS}vUp)!|8++R zjO9o&N#-^9SFZ>27^CZQn-<`6?_p*Fz@NXt@4LSo-GA*6SZgBRPogpUoGuxazdWog6f#ZAn%#!Ra2Bn$+-dPjoY zuin(O$S0&giw7~IFj6noLmgwPKl=(FvryrC^s#q z%5JVwJ<3=urQaLfHOIuqhjCabEI%`v!%t2&6uXBz56_)B?|X@O9kib@_$}@t+a*9j zN-x<7uRob>*DQG%-~Un?ouBC~UVo1uC>#}fmZGc=ZTS^lMf!1&`?KT0+xHCApULE9 zoys9Oxq}no)E~`&w|+w##wzhv1dBK|W^y?l$=@~vvziyu;PB!Xzb(o4;E)~>L9%%A zdS#@JARCt>#V%%Pr{hUDcATqP_1T@iV&JuRk|)O!!%G48pufxqi-;IRXMjAs$3y=k zEcC;e|Iw)2_}tv} zMfODI$R}LqQ(=QCrwc;{Y!PDDdgfspdLdUovuYzLEPTRecX*O9&p`I@WNqn5G5u~|9*A>4#C|R9Yc6ze;7`@;6 ztLX^tNQTctS{D~P;^PY-g7iSv{v&vujIFV=)kC;wl-xbC1mGj3CFb_T?(7_}LQ!Q; z%S)ml5wFbhry?w<3LiIdalJNHK%xg!XTNxU`RK1}eS|xw=u+@$K8?;qxuNU8YG~1! zm4j4GF0{Gntk&D|%eW_u0;(Zmn26p|*Hpy0W)v4`L0!9yXOZ>t#D~~oFrsU;w z@U>f*ZTMN@cW_z>8#VU&x!r6!4DSlttY{JBaIUy{ue6fiscPwSk-E>)?-GY5W1_9x zLRB-mHOv|8m@sSeEBIzV6gN_kedKtbqxULNzbQ5F;|%e0gn#6xOr*eleD816!NOV| z0~xE4G;uLP@Oj|J^6xc`rmTkRr~!@J6$Ety>{hnx?Gdl3;B>E#kkkp%3_7-jS(7|^ z9$NJ_H4CW}JdDLtS5#Qm;Ra-Hm1N@Gv1qAM{n?A_@TV zW9t5xpk9*tS24wm0&)BUTv-5u=~4&mP_~qqrVD}e@v=EI&wwig}9FfDu&g>0NQWOxUpRn7d zFz%lczd z&_{%y4MD5S1S~3;RWd=N(4Cn*z%L^Swv9v9Gs?@P&Tqps6loy4 z`~vBmX{TqmQ@o`KF1Kw9OQRF@sMm_u-H)t2=3`8%9sDZ;b}%l(FRu%#t^LfWpZX*p z&%QjkDl7I<<-B#Ct48ysm8MnnURUuc;$E4KFNA&JCb2w@ZJD8Bxs^if?8CryaTnQ` zHm@!5m#0sQWO=`HU@^;qh5bzg$^TSqq7x^Xy zowqRTD=kHG)_(H!!vruobK{t|HZWInp}oGk$v8Z&96g*zja3|Pa~R=$vAiN*8s*X! zd?GQ#z}4@neWcl?Mf3sJKPQG!fcgeLgyWG7LSJ;ey>{paNxY8+L6;FqL?#msn4(N? z60Dt3l>{8(^Ev7*WF}!US-cqma&V&T+2+vak6t|kTAjN*C7W^!!gr>q;zp+Eq!))P z*o0?D&xQEfem$1py;XXS05o@QfY-0`dj<5`7Vs8Y-$+@UOBt(dkC>w9?=P9e*Ki)uaN|BTSqbd~#B=!T_^zDSYQ zRFNAK>dE8x^p-eALwz~)DTA^ZVrC?h zjLUV9P1I{kHMlQM<;HDVprmkkECuravG$J9l_=d7Xl&cIosMnWww>?|>s;t|7;2L)@|ElSg*NqZEGHeX>HeY|v7H*f>U zcII${T2T6u3fyPFTG3FF8maZVw@*92$vO;H3&Dy2@}N0n>TVDU2I0I(^;6)3$EI%V zg=UTB(V_-rH+P`poxW zTj(m&+LzNjo?)g3x*DO=?=okO`m*w+%XH~b`SWqFyn4hQTt}n2sv=EYxuBI>CJ;MK z+}gP|ZJ?H)dbNy$evXbOE(?75<$CLBU6xLLNTC+fe8`Sj+v0$St8qEVyq59k;y($S zOBW0Qd~eI^Vd3GHuwZ$nzh+yJ%9~z2V7dQZ&%`$gafnc9Eh+6u)cQVZF262_^oH4EF4meNlVkpQa}ZoqM3hwZ zCov3#8_N23jyOQaBoC{Nmk+FvXnfOITTc3F!~0q1lN8Qiyo(cA)`;N+59ZXPR?4%~ zJ*i|6o%};e8EF=qul(V=9iGKE#Y>f=z0!u33H&4vD;sN8<$IH1#uUWCUgqi;O=Me?!=@ zcSNL7*}TQ>jXrSDIe3H$ImlF4Q-Ee2Gid$}stb_-ltl{)3wvLsDaTHcrwigi5fUYx zzm&m`U% zxdnF?(M0pf9)@=iwzNMA97`VbkPijb`W16!I4NX{Vz^>Scq|Ax;KC@5p8eVT-G3Sj z2g&zIB(KyRaATH*m8jY`spc0bv-h$S<QGrm2JZ;8J)!`89elWuvVIMGjG=G0Kqt1kvwqJHP917G2d zj98B-e9xmgs^9-%IWnPHeJLtFnvhwHMy(HCMiCQJZHWY#>KGR+*^&F}$37oGDFq%< zH4?W1MdeY)z1OWJ?;EN9Tf(QZ$wgr$4$BwA2ZngG)HdGHxg1;4-Ey||oCeR`L3YJX z7G425FRcg4RMV{1P~z3rA3pt$rd8(}$r%iALl{-3H=;(1m@9!Hhl%;L9*xh|&o?d( z)Xh7!oD6hRjN&>@jZN>h9S^*A<+8SgyQhkWsq(lQa?^X#?z{EwKWB+`Gx0RSXN9v3 zKz5-xNw=a0VfHI^#n0VyaL>Ah2ngmIhDN&>)#u9$b6F+n-jEUF_eKiQ%1 zl$Y>UDAzw{eEZoI#fLMSx!jR%LZCmo4bRt2klM!Wi|I2|BlVe1pIYW<+wVVj@XV)4 zziYrr-2j;AQ2g5`i1L5w`~0)uk(>n3g<(P(>4`GXDOwCJ+0(EX86yc+)N!lgxBxF^ zfX9(a5>#Wp=(xJf+QHIXCJsKoyUKaDxRdE@FQG42mgSea(S3C_Cb8dWu+eREEi9dF zMptfJtk!DJEMMC@G1`tC{mhI*{$!VFHg~TthXRuK3z2$kDAY>V9s|&SLAwFn8=D0> z@dM3}hTcDU8EDy;9W-qe7Tz(x7?=r$XuWjATa(!j(s6m-+~ytG2e|gwiie{yw_fRo zp?>28u=7j$?0ZG*yaV9@eQ(t^P64&}DeQ{TB9ENv960nUb#{xBv8naNIIr;~@Fn6z zatijtl>*kmxp(yHauT0{4=A`fqbug~x5F7^Pd>*{X){CCxw9zU1R69g*eLrfvWggC*x17?q_m~XfnOr*?h+xvh#_bq;;z+ni2 zM4m~x+CRAi|LWg@WvEADhzI&AOuBQ|;U92b{wos-F-HwoI)e5ikW}l798382pXa+^ z-_wDD0sLeTz)$4=57G6<(NLwfZodXN8d7}-z){x7c1l;9Y%qe60JoM!NMZOiFS|Jk z(YME`=o)At>k5)!bNA^S7js^H{e@Vf$bn6DcI!OzU2a8qH_VAZsulU5D6>kzp^gzt zoQ(^2$hGl1P4@0@QBg2H`;b`%-1b|nDDqF-d9@bsN;-=+Ea2f>*P4ZZ zuZ1H@Er@h1fR(Ssgs#+r?X5AuyCU(-NSp`79ygE%r}2hnVDjdh9)&H~eVeuDm|U=@ z(tK!GHvVRN-A3haIh@{8A{SdHQlCx1cTdUzASu|b?EO)iHkW!MH?w!?g<4n?5#=6^ zSqu(ZTlJL$iV9f-8I zOslnSo<2v0OqCfI7eSG-5x^n&wJ-489kBQg?xsDT1Sq#IZX|MjUZ#Vym9(I|?i$ad z4%<%9`mzl5T+<=3|4F3!YvgV^an>~X*q1NQY%`Z;(5nIH^nOPex@YYS4!X0$k%+u! zCT&BY1h6;El8~aB4zAFo@rJh#Oy+vSaYOq2EgU1F?Hj(AVgk@SooNM~(PepwrIc>B zv8Yd~$(1=FXR~-y^Khwj|H&NpB>|8<~Uj5MdhNfhh+~2&g9~LxCp<})X%&LK%;kk54N9zzDcT`p+n|2vbVrI!ZqjD zt?3fFi(uT$dDXjLtl9q0Y-;=EuwxFuhXhgn za+&bo_|QMIsq;V6X_uy*!aCrYkny=ayq`v}ICn~ZK}=08PZ&S*A^#Mlai*k*FOA3X$&%1gfpPg`~N1QzG?<||{ zy&cjk=bgN>$!Iqi_!+yRuC`{CK>borTH`TlAxs2c5w|mA%@4kguUginOAiT=ADHB}Wor2sQ)nsT-1-3=6#j$WB{J7G%eC8+fbP z7OH73+WDr~s9y|%O&=1eztZKlRm$rKkjWt*7#7AXucgBpM@523Yn&DiO}oS{qN*60 z6AuZ>E3cs`ZWvIFCE#BNc*rA*EE|H4dkluQyV0WV1sY>J*)DA#{^7x%nOxbSLRc zA!L@Qd}0g0&-Ag+4|A?#1|;6pGu){3v-Xd~=il~xj2LQ;Z_9Nrhg4a9k)Fseeuy%; zq(HajM#_eiz-Sgx?$XgW>y{an0+A`g`QjeTN~C^FNsZWsDXkxr8_I&nr|)cwDsyh) z5NVLDgl8G{-n<;pKL5A?3T0RefecP}li(pkF`1ICoRCvW0nboqeTJK~Fx?vCZJrJw zZG~~0A5(s$F*I!O{IE2o)uRzv$qkJbx1gkk*QmSIw+j|Q>1kDpC1z77ZlFJmv3Utr$MDZJjPXNg<~5KmMR6D`pXEI2V@PxKa73)<|Gd1%pie8$bm4rhZ2Kt# zM>%5Yg5~&IU*F4$M*p+scW64~lLkXIbB1xB20ZnRU2uGTNgTvWti~W5oDjC^RUU!qV}6%Imoc(}#`EFc0uvt8bI>z;BDO zG^1xa%FuC9{#0rYagkR|F7`@F(9<0{x<^qrZg^=W_o!AVj*@$@tL2L5TdJkCkVag` zE^=R7osidhe?yJoYl-mj%bv9vLNNxWTe>3HYwDX(0~_Q-aKem7fgkU+pW^XVGmpV6 z`o^!jKgp}$X?nDp0J;jGg#U8Q`+rU#f1H?;I%gWVJP02Kx5-@KV}-@uI9?QN^mLeW zTU+NjZ*lI_WP%_bH?0;QkI4sNwy{U?)OIbUVf*QI^jay!xu!p^rcRAH2<0fVKl4Z( z{W?A@xT|*Yy?HZsXMT2P_F~WZoG|6xp5@c=DOVozdX|5h=jwgY@IoxN+gD*xwkGPVn=3cRq)C7pR$oXaAnCV3zu9Q(;k+P?nTr+3*F31&M z1>qts{%X@XnfV+NX<34R300ZfBpFTVa!-DtyUYPbXOF}=Y{4(A+k=I+$)YRRC25WP82~-ErTZkRJq~XN>T4KXhIgo zNdz$o---`^oX;|-Dv$;|SAGiWL{>+;atgW12H)dnxars^bi3Gl?tQ>iyue%M;j$XE z3q?fD@Pl&x-ea7~xZ4&m|ESe8QNSU_wZ5&GOrA}i0YddE1jBz{L`vD8zS7x=63LwJ zWl3|pa(V)}1lrLx;(k*)s+;#xc2%1OxwfrULKVksRK^1nE4S5%pWi;mRrKydi|J{4 z3tFFu2qF~%BQjt$B?FA1ip#o0bm}Vepe{GAT+|lUD;0{S`;ZRtlYby+@~TzFQ0~h| z9Oex5mX(HST?9PiXY6tvOms2qhhMU=ocyU75v++E(rxp59$^Fd~fctsIC; zx-(<(m(odgp6*}1WE@I7-I7|Zp)!U>-{rUl6Y#hO4EYoDWcXaseW*mFl8MY;Gt{d) zMnz!BrMEF!;dGE1;9q{xUN7J$!rYMAh!QYr=XLQsHL*BkfB7K)#K12A$x`b@bzEZ| z~u^ZWVsm3A`Y| zHvFiB|C6o4sFQUM;}T;b1cfc48LFdiF5^RF%3jDv6LllgzksXAXSL0w%LC>Q zFyeDzD=IUdfz#tpm4+pFVaQ*`Sn&w>R|-N@BPVHAvxF)trWsNL=wuMygxUJg`<8bQ z?GWCK8ZL3}Qm=>m+IN2Bin9u$f-3Nae=&}`fvlA~f%fJ5>YsA8TVQmk5K!ZoG|O-; z-C@#&$qb82hikX9iayiuggZ=^bfZc&&Ejn zlICt~(uwCL<0S!2-Tt0|j{ehA4{LTHZZP6H3BGrEM`s`L*#O~1Ro$)nI-ycYUc_l+ zK!zs9)+Jw=a#~9)&=fjokJUIjsnj%cn8Y_Pt5WL;Kat;6eRvMfDf^R-Of4wNc}gDI zhyOzp+4!uxuQT3R^brJIt9aT6X@6@v<&mG+uY3T;Yqp}5;9U&uM z@XZ{|y&?{yoS$+h+ne1Div9Qt@H`ST*6N4&b&BeQb)Q^AT252HR+&7H z#6E-n&_vcygHf;ksqqVxgN|rGe%}!)tjL4aa0f^Z;gb|CqG?Zr$QOq#|C~k1C-t_>%GEIwUNOb$umQZ>t*NW~Y&xfD-AID(KZ~M-bo+We%I-cs zIcIdg@DsyD{Q_t*XCI%9~QIFxzd|(ca!UBnJTHOW*yIF ze_uQ#03RnlwBb#LF+360SJdhcPryj5^mk>Go;R|MwO?$D?HZU5b<-+X-ts7`-v@Or z{S^RPE?(*bCJ@4(uz!^*=?G!6Zn(g~$ z{os-j&uc4W@EES%T)oVIY?JV9x*U8(a=x$ZLO}5%J3kB`9wl4fTysy>gdEm~WbAaP zRL~#MGcE>hjGd;<%*i2w*RfX)=P|jnaN`+B#OAr76V_0Z%V_8iVeo=bim&Oz}Z>q5YHS9+!2=Et*j@JHj5iShhUeoEfR; zXweF*JP5;&NTO2a8zG3fYZ9v{4PAvXD#yN5RiTQhL<*%OGWlDQ6!vmasMNUIHzPQv zT=+L=+_cfDYjX}$xb=BQllBNhV|o$k6FDrK`mMGm64#7z$n6dDwKkln_%A%TS8?mj z+(TT*YAC-7ipjy6Xkwy30;?_l)Z$ySu8h?s;!^*?LpCe%T!@aQQkqcL7+8>Yj%y*0 zhi0jhS5p^7R}rn@G5M}e{Za105?)$I(H^JT=V+e`R#N%FK<`kK{QPbES-pcjVXN5G zt!uo{^#RJSkaZfyyfQvhoPC~L$|9*57q||Y++q0nN(W`VGx0lLf`*|$GpwSA#D;_# zNMDr?R6Xk^}0Xq_WeADlI4IAd|s1v#C6L}q3!ljNz zkeB@sW?y5%&>C7b>YGGt4ZHpL&TFQ8Q>UyCwIhDIFvy{iVPJTYyFjiwRO8yB;e5ex z)p;+Fp^7<@TP|y)e!zJ8L5^O8pjgwVZpNKgau^H>rt^!|#y7AxK=a@ce7fFeQLGLl ze~}SNS}1~M1X@vkRp@7wu{YwrLC&$NR8c26n!i&u>S`V*KB`h;NxjH1iG1igi+T)r zi7&aeuVdd~{w#Vs?LfKC9lS(o1+1-_*84O~MX{D##MMr6c|P9$)JDkmDmkYQC@*&aX8iuv zT!zYTHRpepL7EbO%f$bdb~3Qy2!=FXmn3Mg7Y9ez5{}KLQAt|2ZG=QA-M#7hxgI@ zU}>lKZ?1hJK=laz|%EZwp&} z0u9QiK6ek485hKJ9&J->GE8eC-EpIO#Fbuqa3($5c`a1~lL%YDhB ztkSZ{<(U|57_iUq9 zz?=NKhlNG}96oIe#+0z6fdM*b3Em)H3aP|*NU*cTZ5r`Z+~?rqBgv=FU8br+sxHic zbY*=b4@7}kPfGBPYDXum(kxBh52N&>j(JeP0k#g`I69bIGhB>8HPOvU&O>3N02GB@ z{oJEZe7k*1!d^`DXZ$-||9j%P#si1PypZ$y9SKzX#vKGwuw|423i}awKUj!&M6i1> zW!)}zbAra)*Y%c8U>h3q&YAq!wAT2IZlI(uqOoK<2Rg^!tz}|JkV`Yd3l7y6Wt+wu zEu~gO3Em)P1XRoz>TZ_!m*tF*emX?|z)u}7+xEAz7L;2{!&NiPmc_qA_cMvd0bzrY zHEv3&rs6GNMNCwj?{h&g($Jh2(<)Fwjg8taZ$Mt2%W&3x9aT>oOaLWUc@Ki>AfB@#KXSfn*-#Dl*&$QNT6F{zf-s+h#S)!`#XD_QFOV@1b zl{Z8-lT4x}abdOEPGMOH%J3hO5rt7PikAZ}u}+G!QkKyz-@1)wl86`EDu>W!twIO4 zv&P<7T`lI|SG9K8MQW`@th5ayM;s0!4KT78EvYDK7IrA&bQ~iq{Q*jADa*lJ)}6A> zPQy%FmN#*BSnPkcQD%VQUp-y^y-cqObG`-BHm0t!EkvU)d$(5NJ02!|Sc`l}oPS$e z4ORMmD>~f>sq?!vC9ul!NGkm`V+_yk>z{{k$#2mj4xxa6z)AnIft|}gr1<}}h5pC^ zLs~j6+MIw|cIS(1FY(ulYqAq4S!VLIB7~Fs0HlebLt_MYNA9<)O}$Mwgw)1#DaxP6 zk`)Ra6+##}~s9>t`#p)TO0YZ>{+7HtAAvH5<0I zPBwFkHEPDL8FdiHu`j7RvNJ31HI%jC4n?5`o(-Ea7>2La%0oVwDKzV4u`7xIwwG23 z(8Gry0vIH1{;{a9NM=tvg5tELBp%XQF4Bao@v8pk&;r`iBp&dD+t-D4=uO%|qY#%p z(w@;j0XO_0QKq^*si-<%lJuwOLh=#fiLC+R8jq?a)B3SQ3KO|k*3{Yv=&a4L10|LR zs$G%FViGIXVWbi;uV4YY>Ka+@FO6nA)vN^ zR4A2deRKJGf8L9ZpMP5jrG(-&YbfDbz8O^N%lE#}JGO*_=RjlSPe}9(tUnI_~8&>x5~%ED0kz?Y1`9ptBUG0=R2dkGvG)Tcl7^(Y=j1=oqynXR{TqC*MXhH17zUmbIOkvr4tAO~)vPmpMkjrt@ zq;wbpWhyQg9AS_9-or|oekturW2muUrZgr}d$+*qrtEewI;AbQmT4*cg32{cZ_<+E zP7=(!^tfOyXL0X`Hy{a%zjEhKxEG+R=zp50QY=`ZHiU9ApGHhNkl~ya7g}F8yB2^H zRvg@*H4ayf54YvwfyeKh6;|#fl-BAZRsDzp$Ok1 z9s7V{*yI8OjuXehMlJ0d)%fNw3ubz809Mh3Hi9Z8c0g}4JEwL_6~~1qa1HbVZ^jgI z;r(rVZCfR_TErMG?mQxlcVaGszoLp*0xJUNiw5xz52~d2ZB8gDbz=_RqP|f67Y~BR zltv^+riD;07^t4b3KFdZl1dT9ywi5Hn#rcZSnE&4D)PMhPC)MRW}uEq=@vMN(os~F z9h>$C>hQt3&kfZ@eeJvBQ}x8Tsa9-i`lkrV%{Zl8QQkBULa_n%l}Rk|@v%|#j|Y7) z+SykK*coU=Y(@JK&fZ{8m!MQVvBXeI!gSt7?4pgOY{C;YP;*x(wcH)e=E_ib*= zwoXu=Gi6K+9Sr!$s5(!#xHvd@&VFU2)`b@X>qaQrJJ_QM0OJ?-dY9e|g!J^zMa|?0 zhH)uaa&MojAVxN{^IvEtBfl9XtF?ulyLZ51D>{;+x6>5Mm|2sl#!GY{U5r9@4T7^R zV3t+O-1c*))D?Gs_lOuTe~`~(9-;6r>(Vs%N-bX5T18aaxf5EBXNQo09H{#X>oKqy zXn4(N1Jg+H8GrM36+J2o+TEvd`74drckoaq8YR^uewnycv98**Xxg9IY8D&na|8Z- za>L0n6%_E3{hyBmESYuFp?gb6ie&$lE3i_OHqB-7b zG0jc;)DpO8sYrLWN6HOUVXo2VejYTc?e)bek7i63+;KR%Y-Ly9@y4`FbG(02=kmt5 zOS96})}Knc4wxM5J8IlXFo}%TyQcls9PIjs)P>$&1y#c%ED8_%^a5wLqj$3)M4q7j zl2DIZfB6U_62?q{`t#+IGs6vEthZc()$X;5a==NXb&pv>pF~N{j`OLr2P+i0nwJC8 zQC4lW0n6xhb#0f^8YwoF{#Kbpw`5)8QPY`yS^0=-4~Y+}*`__JQyu}SoFvWhA@nX+ z%%j+d^)iG5xo7hgvxNh;zjb^4(rYF*U$LD6<*@&a*m#l{W*Pfu33m!o?W!5}I7_`s2gyZ0=-w;CdU_ut$zPi@X?cF0iX(eMaboirW zq#yC~$oK>GWL-|nbFT$!T&+sXt8?5df}a!zofJn@r5e<6+NsQw?w@0H)w0Ei5vwzP z?G>2|<1k6x0jZ{`hVHn(!XfKWC+#e^q4if~SuZ8~K<54)JcMq9sN-`bcf2B9FLg3I z})WOPbh68um zgUKv|ZG?hZl%(H&!@jmB{j)6O*0N%C)O}$llU~R7wTpYc#XJJ(D$4oQ{!ulo-Rvw! z$f{oD4Jd~GTGsJ#+Iz@hHO$=1Y2ixwIPx?#iu1tPVuOkD3gP-tJ2|&+y zl96`;BO!;}6RX>)Q%P+#fum~fdO1%Wp8VIjS_(s5`hyDZdJaxxMTt-GBID&wuQtg| z-&=->m%`fplFSJA_-b__Zuc(Ujc*Z7W^>FPgPCXY$=40e8{A@|$?j{MA?};pBqc{^ zMC4+E^!i8cnaD%zUB4#v+N|j>m8otoXZOM*=&LhyZ^>&YPcAq}pUbQ+li#2J^uAXR zd>FO@xR1;O?jwJ3tnME)O3ujM$inR3`U2Es0QOq|F4b)gPOOv^rqE!MN`gZS5-hHU z7|hL!!UQZ$paziBGhfbWa#E%adt&3c^SRr=|A2NM^<}m-3sE~xWOwLU`fgLJ6E-YPkXLP zr$=*cV)$5tivecsUnNJW`;`-eN)33gNdpOeu7PO~ZhbTCUUT2Kfi_H!pH^1B($9#Y9tR&-{f>kP#^PmG0D6Q&9zuIV|H=2<s$hSHFDhQ!J#^hB%c-L8Gr&AmlL3X?DU2#kb@*nHe*Wp-Oomy{i8n=q*q!`#lyn zS6qLiW{Q%tG*B#H9oGaH}yrYfn21odwg2N~eb6>na+|iz%`m17|RQz<*Cc9NRvARd;9G*LQ$Mf`Fly*Wf#~;fZ zeQipc^u?5DYe3=)gm%pL_j^;N@vE;heSdCN)K9k?U;|W=Fff3C#Q$15*?;VwKkgwH znlcVJQh>eVA~(J=9AV;8eu=F!D2!p$fUJFCl!AnXi#0Jmtn~g%U5)k47>;rk-jcp@T(>kJx~{#5a3U7f9?F%6}AGYG|Z60ffzD5?5D@cV-7HVzJr>j}N>Kh@mPqAbpk za@I-+Rto$p0*W%!4np~i6G9xpz)MYAiiWor_D6WW+v}j!@}z%0N<5(5LnykJ;8K zmMakPkv*$7XR!Z3K1lQR&0EMAT(a>Zr8*dval>>b;_weYmr^>cTn}n7UdhZb;)}hB zsh0RKNPh7%Iok729O$RA(c>oWEVwhByKUd!KT9}=J;6$mInh-9gw8=wPm)btYE^sElOmw59|D?3Z@y=$(4S-JANze?;2*1Y%zgK61Pm97){glq z0cMQ+PQazrl@g&kHA~&;_a`E|4DY#)=x<;m-ObKvxI z5i&KzV>{F1yf)XBBfG**WE6kCc?(yXiqX!7U|R>(t{{<=!s1so8{$$e7=(s&dmK!`2Vcp zf+EAz{~jp=1~4b7zn1r1{#AndM|}TC{V{R>nz1a(TS>?==j1BlTSTLY3}dq$5O2V3 zuqy`0qf5bb1Pbdpf4N%cY}obbmx7nq0Ud?!*YBZ`%&WAk;W*M4s{#eHh!v^2tm3=Q zend#13@=C%;9RJ~wb*0waJ-H-EVqZLKQHfIE-`|%%0MBc3s=`vrc61H`R?citXcv- zD$$893PUa-(bIE^i5)-1YsuOu`*kicaWGU0Gd%ujN~f8CcL#QybGMJEcGQF7oE>{g zb30^0cPFsrSWr9Q>rbw@*dCS-;+_wS;q4v8uLn{IJ`f z_lnSewwxMK5k5!;1ob=M^B1K)evkdTm^fQGx>`E8xc(EnasZ7mfSGUb_K#e@gg}O+ z6YVH~e|&!8r$H?C84mIY?(i%7N}_)FZKrL%6MOGk`y%f?*XFMLtiT8NNDa(_PZ3z* z)UBDz=101+cQue>k7U8J(E8(B1V6Uf@eySUam*Qa^GZg)rY=Bz>!Y7>&W@wa+p7es z-y4UYsRcnx6>NiOu7J;s6=1}lxv3BKMQaeO`P2Kr2pzZLFTfxF0PNhq$jATm=l=kf zTbxiVz8|Uf@q|-v zd@8-9#Io<+Y>Hg$AabhlE#Jw<(RJ`3BcFEcvE;<;=?Gtp5DhHfb{Tzl)jL!H(o4dt zguBH>zG!lNX(9PjByp4ke6)6Bw^j9^Y6F*LGqvIT=mIq&G!vN}T}w&u*Ya5d$rTZHPI3+V41YxF^b|kL zPJu2q;8;kQ0K3I8`%&TaKJ7^j7q7ZzUXe$hNgX#RV=CMF%hBC7+P#w6Vkdp*$<2NA z9l&sE%+>Ja4#FBL=H%D>NvFcx&x~UMfMg!xc`7OU>^OqLPf|&)D+3;nvS<8 z1_8ju6+@cZBn+jVLl4)&srhTn+v6xgB`9e(&f=>1%_4$Zf2cNLdP zT!F?_fW`aumB~E6r@+ZoE`lnrQp%5){*?pbsiTiBl{||QUq7kB$kaC`*Eb<_Y!iQ! zPNW^0v0G}+UDe6}cEH=;vfz*d#*b8Xa?vl4PTGzZt3sFb4*o%+{4h2CA?)ZIbK?4S zq2f^xT!3Hv?Z{j^J_DyVom$I2>CiYLbJ|`!fOn9V)>o~o_|FcMKI81e7=XY@1F-rR z?OgwDa#>YH%-+pT&dkpEf1XHJ?6VFMM0-u;6JU!I>B}7{P9BsKkX|MHSK=3R+9wVwZ!|i%>m)lMi`~H4rdqY z=~DycRAHMcjPfzqZHsVs>T|$;_cDff=D78`RXxksT()mupGY=_5(P=Fl?52DmwJ8;8_S3B6mBKK+^XLaFmq}4JWotSG2^X39ZvrnUl@n^Z^=sBnyK4uS2_$ECbPU)L-KCwqY z;^q}t*qz6Q`B=r_<70ALcNY?CUhj5j@A?$EkHxfO0@6uV@RPU=i?jp?<^d)R&A~rG z1ID9fA5Pl^-0oB6x-Eukh;mXQ|1na9 zs0)p3-XU_E+@+@;u4P#O6=!wQTWYk%Ia%ZW&yIzfL(lSvfZ!4W5dVJ_;Q!Teb@I3K z$!{xYTFFHba%UVKJj*0;s|=Mz5mBgm6ee(GHC3O->PZEzd&S^E&)3N>6PP}xZYH); zvwZWbx~!4MW`wCNdncNv73a3n^JBh_Fl(V{I9?U==c{Y#y;hrw_tHX0W9ti6UovcW zbF~ughgBnwqggOU%2~`9XajjW!J7eN_IRMoSN?hecK+~;Xtj!!z+Gr{IzKxhm5H+R zF@+dW%WFQwO@ve^b>g?tuk^sLW@hpUBrN%XAR}h8PrW>6!^f#8x2fg*2V-JcdppR= zmB)332qi2#FL<>ZxAm4s9@u|vbvp(YMd^*|)*ZCPMj`9@oeB|V>8B6qL#{FVJl zdLHe2ZxHZ31+v+H#z;8`J)};ctn}9>{@T29L9llS(@NT@RS)x7elA`mVD!#lx z{AU&^T>!TFEqKcEcPYpp3FQBaf|NEXP--DzyG(EoBN0kh=?rlcuZp65bZ|;pMP2sA zg(Z6RBzjF_cBN)l$%>3N<)xlh6$KNpY-XKRGTCACNl?lJR&sq7^Kh7m_%*Pud@HGh zcGskbtaB7zYX86wZGF2|NO#t0S8>?O2trr~v1ANQ7Xs0^0+gAj-YY#p#*`=wO!H&! zw_PIS18TQ%H_n&E_g#q}&v4%%+EoG`?6$G{@V-M{@8{;3*E$Ymud}$!z66}A#WVyd ziQt=kzl!ofV=x%4=Jj%__s2J(lh(4R7aDTAvEgPoCMOq>G10~>7+U?@*WK~x+Epdg4GMZ|J0 zP{oi+q#U#88ZuTVpRd+`Ds-Ze^iNH_Z+$w81z2FK97|Vvqa5vJL?nOVvvph3YdC71 z2t3zS6&nvu^{hfBi@wYt^O|ipb6vx&^F+;Q*;!^ACCMJf_yBMao`^^}p0WDqv-WS! zThP}cJcbguh9bmBa|A1o*08o#)B(i+6KwX(ZJ}u%BH#pUPosO5{8LX(9F`2t_1aA- z?iCn&VId!|!r%f_$33hcrew7{p-CxuwE1F&7u49UJ~yS4tj zwLZavKe2tdlI@6Sd6$!y*GuN^pbKw1Nc`0 zzY^9&#hwqsG1e!k+}}X|XKFSsveW|s;GF}2_rE3a_Fu96b8`Mo z>i$tY<5V>1>^-?i!J0P=S`jhfMQT?^F@#>nDZJ_7IdAq~3(Dl{^!PP7)v4e)-&G|m zGYd%0HHGx%+e9Hs0vI z(c?fi*hW@Ytnq1p@f*We4#8+ffJur%m;yXIXfRMTCG&yM8=mW4CmjMVqHEG>#P!|$ zo}{?=A#dX^lS?-nywZC?sKl?|at-?pJy^zpC#FEvDoR$5X3A6axf^z=V(`v!PnHjx ze|=$$-~-WnJhhjeJPJUL^`1jxg~`S|?FFo2o7m3j$) z=il1Sf9o>-pW*%k!D;}#GJu}mKPr{#dWx3vj3UAH_ljo(7&nU6N!Bf8lhrVLexK(8 zqmV+c*?*-{{AKhJ3MwOr@r&wP2u5?uoU5WL?qkbhBc>EC%8pwKI7W^QF~ z+&@uEskkroscRD@NN#>%E%8S1IK@zhhz_R;0NM^HKx%*vNax({&Q53a}|A;1wW9mG76JdA6qfYA#B>vU1Ps_!?%l6?bq?JjNao^9Oyc+cF0Y z1DY#`;DA2GcN-Nq>$&M8NSS!`2;5RdI{V8bSb40OyzuSH+BC<+8KjwgMcF)$x@R@e|NuOer0!0PxD%%Z_ z_#Q--U->~+Dq$=lTh5?V@uj9P_P&3C?sHi9_sx1cp9TyGY3e+nE0-O5OuOdiQ8eSj z-Yoi^C**I>!u#*q6)?lsfD{Z4mjN3f;Zdvx-#`s0sIN$Fr}@7nf~cZ;>f^WGB;>Fx zcBbBIyFe#V8gASad&FsdnkwqzL5eX?r%_sW%@d9Pe&<-L0o@f|CreLq6*zfrBRjYO~Ss26kt@|M->#v8* zt-E0~HK{H7c^RkgqlF{W+zR#OQ`y-rEQ6-yEfnK~<)>t;yY}C`NJgB@GAUf-bauZs zDL+rBmPo)x@|OJe+-XBLy#ZKJp=de-f#HEVm8$nfTEL`HFZn-^KQ(xb8d4b=DGRHD}*TPRjlSdW>1dC)BD?nF`+{;lMDMZKCI8Pw zrixjpq*(xhMF1iHTW=Zv3iltl75gfr@JD@aS{?SBC1XAcy~uhkKZ$^1(CGgm?k%H& zTBCJgrMp9rlJ4%3kZuGd1nKUOZjkPh?gnX)4yC)h8>EqrZ=pLm``kOOdynz`SY!R3 z{mgjhEP%1qmNM$lB)H>}rj(;7Z`y1yxb_>EM{@$uv9{CxA}8h6GUI!@-A-CAFm0GS z9pifE1DUJy5>WIoV3lbTeU+pCor_os$iucs9Y#vL^VKOtx`M7}pwUaupYjT+H0dHs zw_{9M!!wsD+?dOy6jQutYW!eYs{?V0_5T&w)KHZJg>%{4-Ks516)@kbK9$gFUS$P}bU?mgAXgU)h4dg8g7Taqd!h-6t#k5uQKJEx+wt>d z-0}7RyHQ87KsnB`(nED2_#v|-m{*ISghMGzj@DGRM9n?bAF51@KuLzml<#=qJD`1d zp4J=83$80|A+(Ln3pzU|**a^AUQx`q4!lj_XB4JzbbMahVg>*u*Y_g~;@vOxghu03 zo4yY7)FM`aZw;^SZ84VJ-JiW-bD!JKT-Xo8w!HE42ssCXq0~IsOyhHQ58x95NAtkW z(Ww+U6CS*uO1w|LytbI*>Q2PF9LTgs8_$cPd%+F9QMevufvwNq8$OlV{C9AAfoGiW z0j$NUc=lhc#`>$q{WRD;<##qKtVs8?ZXu+^aS~xZWMUX!rI_q}de=WnGEDU7dEm99QEH-J-|)#@}a~M`R8h(a566PiY4~p3}#NG)Nrm_G+J+MM^5k zx!ino=#$9EbzrXx$v}7M-wd$7thaZW^LU4X`21)~3oWc|ncRE|L@D4L@}#XbX-kV8 zpBf{|5{{`v@n-d{HO6I9>IJ=0F0YCB0Ff^~MxYSAiuetR#F@`K!}9QNA5NaXA9PIp z&VEoHuykWXwUqop)AeoRDmQX5ZQM)Z`i+}}rxjnvxp>;#4=;DPxICA!#Xy1Vf+Gn$ z3Gy2#oJhhd1ZDscf#^ zEnG^J8r2Zdj5hTXyzYc2Vii&qL1vPwPrPnkOOVkf-M4!rgh4O)nSx#lRfLetv^Lhr zO?M~^xVX}W7rBk6`p@!-&5D%X=I6a?dSxCik|K*?*>|b7M0qSpko4vQiMO(P8OabG z*9A}Y6rdAY*)n%*Y(j7XE6dw~;{;98*e?`8LazJ2fkXS}TPHSnmbN zIFIGbzmzbmVZMI7@rj8agKy8G>I}cNf_Fr0VAgp{2!hh^*$jo42nlUx8-v^;jVa$D z?0TNfrP9|#4=hVwORcKo!qSl}k;>kj0b|l^{dlnO4&596=yLV2JVA| z7f_qHl%vV6y*TfYY)Y>iq^zbFUT80T3PkHwNr29dF$|$(mU{ImXt|Z!h4#Lm`>i$t zxoUrUhz#Vjel$2E>@;?A3}V#hq8G;aVNmvBS_&}4XQ}qwY04c-Q3WW>r!grJ0x{AP zayf@yEa52?m{Zrj-%b*g97gh{DopL@6w0JbJY!*eCn}d>5u}LBrm;sP$6wDwS+aVr z2c&R|OAOwy6e9mA~N-EQ%Yh>O}LL^8#S8t6k=vcqV6dToMnnr z`Y{cGRE&oIo^D_^yTy^;ez3_zR=#QJ>-v#Jc)R=qDDqq^_fB$2(OTFMP+E3!mW6?3 z2fq3m95mHlf#7ws-HGlb=2^WpKgwK;QDrX?UfAjnM+u~fS38@gXjfwc`!UQ;TfsAZ zY?q&MirXcU$~osEzXb}u){oKC(yjaAliZzvYW8L+Chv>$V8Mc5s(@d8L9u2;hV%Ql z{b{?{F?*3osgj`)j*x}y-VLtVgR7KGPHsl{fp#r*KBwevQ8HEKWJJt*7F|yWO3#s0 z+3m%t>}If|=q@@0?&+tBAIq&eM*8q*&Q1-X@#eugjVYZdjGB|b-^n=DlQn{-$lrO7 z$?q{Alp1bhWsPJg%8>Y0(2X@Af_@*?Dd5N8G0?8rvdR&*mk2e>Xr9$`QGOaeUt`!y zGgg0Xn@aP-iiW}}nL=b#x_k+T&aomow{)b?|2f1chVe5v`)$JT-WP3^YM;cVS!Oi_ z<6}xkFgCE@EVxf$HcYrUpq(!?o~{k0fPA-z852Iit4q;KY^#tA3F6-0BP40=?7sya zk*Kx(Y+mdjiLxa=2m<3A2wC}~+6 zX=$7OImM$F+Gq{{VW9c3i;kAxr?|?}iiSZLtF00XD?sb-|I#Wnk^Eqvr!>4c>C?yQ zTVbwd+p5gM>(C6$oRYdSv|(q4iFOek@4=-N|L*TEb&M9T?lOAYxdNquu96#D?DBU! zn7B&)K}q#t>AarmXCLYc$B<90AasFd2fpWO(fgLrLKfL#l_w0GZvz;hS6)?_)}FpX z_~wYX_IfNU+`?D#1M!i5FNNiy;kF`YDvECM7U#l^2#sdyFn=(ghO1}4;P@PVa&(D^ z`=j}ZJv;@`hr0+5H>Q6vdHA1P`xzel<exTokyUNe<$1I%CXc=l0HM_S5YRwte#lSiKdNNN3Fp>(GS7DY{lDpO{xI9ZFN_ zAz`;VJxnG+hg-uCyKIBPqVt!$%GQ218bu+}g1X7~31w^Gz>yhG)l_(vGrH~`&ds$d^`oVRo)P<@7^#7>eR8h-B(*WF_1i1Z+S1A4mxBvF0)iq)sXi6jb zF6xzFN^J@8m4rSoLwO}HCP>~$|K5Z;0HSK7@^qfdB)B=9kg?(b&KN}rG-g&gg%SuT%)sZ;0yoflhA6CDCUTPHBk1kq*oz81Xa7$%i za$rp~B}aZQ``H|K;k60nLIb9 z>Au<$W=>CRl!0ICjM!H`L5~M9S1hTXR#2CNHGB?x_PB+&E&iqZp>&S~$mSPIng1-C zpVpurJ`7y!M(VhabLOoaiPK7|@MSys|?so1P zchgd3s6IZ1RD#S~`1&bB!*q4|_3IbKR9=~&V3RI2ek<8VoaQn!{5J^vNXJ%tFB(F1 zmbIH~4n1XOZNh9EtK%dT!E6 z8SD~uAYd~lwbVoBBo&B-!s3UEqj{ri$46)zd1zbf-_@?AO!hPnoI2(0yX)o*WhOD) za^7PwWvOyR2aZME_rg7&;JCSabiQUNC$=vy;eJ&j6x0wd!DT&svExLO?X$!?cq zi$q{;t4vSB8;48Zu-u`V8Wz9Z4ZY#v;6gx~XEZZ=^ZJ#{xNc$1C+VGH+;2rjhs*xmA*rJ|ZqGVZ z{4fs4oze-QzJE4CZKBIQgfV!3+QY1Ho1ar&a^}p%yh9$ULjaDCk|w1f5%*TIKp$vZ zv+!A5Ad_Eqq)mw>3HqeIpxA5~wuHH4)nHrsF5O^GLK{N}^h|&5vJn9W&)NkWg8yt>YpByKKBy><+n4PhWib_rz2xH{zNW z;4UAa>HmJK?&C6ut8A}1a0K`UGxd#U!A|-bb2YL6RJsR&fFh!tv7xw_SMu3V!Tmy2 zKT+dY`Py`6U(vam3-)e74o0RkO8w5ff5eo+rycCX2I6E?;Rv4`EjKJz7rVO3W&i#QtiH5P?&O@SW19f+*iiwjtsh#D8}i{x<(4{ z0LGVF4upiYu288`^3H0CJum4The}iLY5yL<$;vwjBhT6e%Ff>j0df4uFp?~s_iIFK*7_>3VJ&p)^oLF=IzE#-M6bSd|fny(AeXAOQ>IrQtt zvenvBJ#Y(y)DEqnKc0?Bje@YM#T;6BJcfMbl$fsx$f*=qME>vB-2YWpKaI0WLCRv~ zXYIJXv%E2U^n!@G04XC+wsJ<$M;==lMKn##uIk>kbCsRdD2u16pl{0UM1P~7-F?bL zRY@hH;>f>Z^V^%A4$~OL>1=W#`Rqp5cJ{(RlUgbKz!k5W{wD2J{G!R-Rd+@6YrU(a zSfeJXUN=$8{3)4%<26wS=CJPV=yk5SvaK^jK9HA&9wpd&m!c=YwI8}C7;jfdQlFS+ zP;_y%ZbFs9;z?idv^0WCKg~G{rdAEE&~ldx$)`St)6}m;{`hQPu759+6>TD8b|N0} z;9@aRmPs!o%LTr)<4F`-E1R<4a%~{FDjzqROYV~ctfy1h7n?~<2K8Ae(2e(}0muj!;|@zn{` zp&`wps;bM&d9MtP5ZakG=vTVLd}*^->5#)5NV+O}<{jIh9YW5dAzc+|P%!&Dc!8QT zbB8iB80l6|*2IRmMWDOT>$u{{gTs~@AQiegNOisPmr`7?R}>Q`2f5N+-R_F4I^@oM zuO_#N(7fjpUK4tSb@!EYed=S^%*w$)WzeG9*RbeX`pjpu zM~E8jIJUQ>4Ty|NV~6QC4VZje0ypkEXOthwMefj;OV>>a%&<=#5MyBQM19|t>ZEO{ zC%@MoA??QKzX-Ru-Mjc$cncLp5rWT1d-3$#R;>y{DBy(LlP8_P_lrHGhgS>s zdfN6D)+T>>Ml2N63ggB1@M7cTeUX5A`KaLt|En$?TV=B#lKCoi2|}lHR&Os3moF|S zS@qT$YJ%Nf+gV&f&{75QMfK!lq1vZk*Y^6Wj1BglO}smg{wC&6{Y^v!c6e0L4wv<+HobNX|%Q%taSF$J+ef;Dkt1whlH+@xqED$zu^~|Y6 z9d3nCn;=J%Y_Tx2ilMd{CyzY>NhM+F+H^?$tc0v8DLxn{F4rg1Ryt8l9Bga}!?0Gm z%UN^K5qfPs4!GP2E&9pBI8iAPw=k^vZBCXnNeLB>WEJ_T)1%1_q4;a)41gmK(@MWM znDBq%%1m zo1?S7f~Pbl%-n)YeRRY)Y5w{u8DPjP@cm+de#8{uJl5apdS*YgU4G^yHIAAR=<@ z6Buch@Ey81zHSJ5U=|i*%HlmWzPn0Y-ur|}jwJK%D?^Wu-6vtGNZ9_A9>5?hN3x9?PGYbclAQb_E6 zg5;l^YQP=2WPlGx7dUyFlnD|VxG9f_A$X6XT;2!eHI4`>2a+Y+%KjS9q252QvdQ+#J|hpHxQDb zE~6%b^x?j@R?l}d`~%phi6HcJ^1u=Q9HVhq!4RDm{4xg8{HsN-oRMk0YhF&g7(&{! z2I`863Z{cTx0~s?kl4a*mkAtPpABnkCgaeA{w)`#cN$@_*KZ0*nF}wS92u^Rc$=%5 zs}>74wrcd7n`aGjY7SCMlF9)8gs(X5w;w1`~8AB zr`}EZd=LMsRTr+Us!FEjLhGeV=#h=@0upD{S!L`|vQwBgj^bA25TqG?W*lPwuG42{ z6o#_1tbH?7G73xv4TY@?{0|HWRJI$5fw5^)rxYDB=uSd;6LI=d66+B(n0n^@jx-mL zj1c)peKUi`b1rYGGW41&&w5omsC`4K*jVloIg)=#Qg1_-9Y~f2Ekcki` z1&c-zcM>JN^}q?X9Pv9nALa@X|MW^r*hk>gt7Ddf$kWXUH;vUd-C{a)MYkCp7APwv zMYjbVm~*Mz;ri<*&W)eeaDAN12^_N~)>3zJL%rAf?$Pu#3JAl7tFzHF$cF;LK6*^V z+vX}~vy2yt$R9hIjt;0mCh9s@H6AEeSHphit976ob!1Ukpa&Pa&r`%^&4DyUY=8&#rBI&BLn^$ zuE7UXwo0Ls88uO^jO9+V9$(rEJAh&CJSB7zPhTT zp5Lx|kyh%qGvgok+ff z3f=lgN&3{t9L5w)bpJ8iwJl@{nyu+ApH;Q*)^^7*WCm4|z7d=3{*h2IEb}C)M*Iiq zh>8=$e$-32aqokhTt)2V6!pEj&ki}pSu-QMHs7JGt467RENXWSJ8?Sm@|n)8l-L+w z+aTw8qb`ZO=^!p)Ml%wsFLoJdS#HiY7$+%UQf9E%i2YKutM02P=j)vYA*P)LkvJ;x z@fV5;wg>_kRI3a&-R7}fu@>cmc=dgCZc%3th#Zs&y{E3WQDw_R@>Yk{){$0Sv`RKkgU zT&oUym*L#wsVh_5I3@;@jhnm)F3!esaOfZ+^Ht5)A-`P$nt1WVrQ@VrT&>+6G)vE6 z;Hy1Fp2VEJ>lQ~}t3BB#SaUPvgYhDnz_92pFUG7c6<1Q?cCoq|4|^#id!^{5*BnLX z8QrTrx)vq34QLzA=Xplm_4%U%_lt{SpNc`0C(&E9teeLVn{o-n(_s4TeeVvO*xH%l zSSQDBzCY?$(G#jbA^}lQ1BimZ*suEelKhR4?c>L&e^%%bW`=XON8`u|v#F$e$aN64 z)t@UX20z!)xxc0@d%b-%gwz%{cd%dI%jT+(JS-*EDnC@{R!E*kHjjT&ZbW5q@cjc= z{6-^HVQ0=Z2j>Dj_~4o&uf=FgQyJD>bZbI-V`C(p}3ln5cT zfd}2a`^a@Q7D*Sx;2*xhSe9w2iyQEu}cy z`x=wr-F=~7XY~#T6JfGK-`^wI^_<~Qu61@HV5{FEseSk#Q-;C3cp+Q>LTzs}3RkGe2{fq;gFVdKgZX(r?JT6&>G|pS z{)I3OgU;|cUo6D#hPC>>;C>yq8Pde^9w&>|aOm}Gt0QzzHEHmgy@ zwu73eT+Nhjv~7(5xN{@L^>!))(m=u%Z!gfZ(4$IA+um+J;{6lCqoh7h4w4q}wnzkw zqiJD=q)e8iO%G2JM0*`0@L;q>m4v77SXJBHsZx-GdY~pGJuqJo+c7|_)O|+57vdZt zTo*&yEVfw6D&#>Dv3es0KZ8=?UHqN+Rm0W9Fo^P7sJF$_PNbGxWId?EV0zZ5i$$Nl zqKVQ8f8?&buO3$zj<$G5q~2!-F0oKsIoTKbW#Anbd09TRU#bwUn)XSebxXl}f1J6- zhKoqyWBQGA#J|@rX?QCK9^L}F0uo{a5cB_A-CkaZ|CN*wt(k6nM87yN2Htt#8i-71 zUe>RExSGZm;~N+!G!;HjJCG)m(N7RKvYvPJZfhIR$AUgU*eeE9vry*EzVu|S81 zX$HJJ0oSfjS-OW%ZlgIpZH1{->F4Cd79Qd1HT+Q3h!!&O=}#23Ra?fiG!*!pRZ7f)xc7nar#Qc4L{((ga zo$L#N6bqGRf#-6#QYPRbr^wBM$2dx8>7=9fTS5|7n4%)8y@y8f`qR^6<%K$J%v%h` z=aBmbE9@);P;#mX%qCCwaNyyHzHj4cA1oKtNz3qGV{ae}QFp+9qirpKhWN0Pb^fSF zbr)DmlL^#@SD~IfdHMg$V_hQ~TRLFzm#L%E&(VS^r2~suR;1hg=9O?aS#0)W&NRM= zv{|ol@H$p?WaoL043B}4Zf)@h5V7f_k(&daJAX5hZ49`NFO^vxUCtM{wxu)nDy^1S zaa&m=dof-{mI)LxG@u4C^cjp!&mpzKj|ta=->wLn83BWZ-QwwU#6`NfL<}u;X$f%o zk$nfrI(+rMU1a+8HC*rEE9C8MLt)}{&%Dm#Tt8du8dF0Lp!M`G+QPc?fZR!jhPI)H zE)|cMfKzGtjzTmIb!-@!pnTFJ(*4e0-n>4aDzhJT7d;=+!<)Jwb!tG=1Ci65lG^XZ zP#TEmRVvHqpZBb?P70wUuS0_qR$B0A6>(4lZMK*&YOAZRNYntUTW*~wbr_Zp5*J%S zpWNrYo({aTkWb0o6sSu#%Qpu1*1F+H0*tZEEGkOp)bcld*wLTJo7eQJP=pI(qkGRa z(JYn(r8#oA19|l z7d$!|1Fv0h_zv6eBs)KPle3qlsWO2j$7!g7i4*9w$aS$z!tH)_(MKpS0J8EjE2V<9 z2|OCL3hGm4FZF5c^q7mz9qTo0PGm!sEOy({_=|~wPHcs0R7UFky#D8(qRnjSs6K^z zgO?4`RKfBJyh(oX{1oSlo`w41gdxH#krlah4-X{!X9DW^r%-yhTHtlk5{?9d0W+=k z!D5{U$~HzNywBcNcU3!d*0#Ol|1N)rX=5C=HlHrqeiy`LVNCAwNgK}4Y%vm7xJ^|Q zntbvzbo~2;BaPOvcNK(YtU}flbw`|K7pVn9a8yI4C$57RX@@DhHMt8q5@E+0fkL*< zWzc9`nRNNKf)P_(^X2v=;%>%N1-^v$hJH%T0z@u|P~QcrpbIBJizMand8MqZbuzz+|6=L#k_W5reiZp7|4|h| zYv{qib>ir-?lG~7btrIA?+PFDKqKd(p(nXt?OW3$`*-r895V;(-V^wEf6@MJZ1k*u z?pUD-l7MLg>M`d$?TF@Rx^=098hmU5h)6kQXtRCcx(Y}KpA2gjb(WZ;7(XdS%Bb+^ z7d17GzU9d1kvbMUQ=SSI<`w?9OT0?^DSJ8YJ^V`vrB0J|2YabmbIn@g6{eWu#;e}# z!$~bDbFK8_N0bDsBs5n56ifgS;};kEKcukQz(#c%9Sd`PBZD8Mx2mCs0wfZ|QoT&s z6i1^8I4&WGH$6IMdtN4W@P2u{07|2?8kSwDG|Qy>$IFW&VqeChuU>pFs9qJqOCZ!M zwrKmvSB?!1AXIoKIGC`85NlXgkVb&+4L@6$cxp3V`t8HH-lr%HZHPcY>1XtroGzqT zvga6zd^7rSLR(AENY#Y5!&tub$_)r!$=MWcTtJS*d1(+Xrqa`JDlTDURoaiTCv2u! z8C#*3G>$OMnDA*l;!Avnx6C(yFAq0$MSo4gKXHzLsfCWopU>IM!e0X0H;~%xT?4%l z%v)p5Khuy56{{QDQ(`GxqP#K_OBx7Ax|uiWZFRR~F?2Y0c5*VnKJfRX!&`>YJ5q}F zfx^YZ79*DZ$5-$Z4hwGr34T<@0N%=t6f6JD0|#HDf~$_5%Cr#8@YBGV3AiA zh~S1y52~g3e2{0BILR$Yy%Y)X>B8Zsm2tz%X}4xMT4<_uDIzOOx>si>PWEE7VC$xrHe$z8%1aytfP&I^lF4UtuBN8BGNaK5A)^uzeZ-oI7g zADsTH0{>v|2mAZLsAq8UmS+%189NZL(5O3_NhW0pQ?Y{)s*0Bf#L_W{;D~rB&Kju) zwtGd0NVEvcFhWheuwhU%I1R;~&1c~98uf?%3Pz|Sq7EIWgz;3nhl1FSuU_W(if-QA z59uNv>(2YIiBaVnP?dcydbg`W7Mn-8ja-)qRn3^Un9h*`nh=>IEu$lury6z+^J-ivzGBtA2v;LFCuHiBgUw{(q!6h0fUIr4|xv)?)jF*Naw;1BCK6Oy{K4XRF zSmc5ET|{MNKiuWU~#FbJvJn0x}`I zWyfCG>$2Aa?@4Ov*U<<_3l~vM$JPA$o+fziM{^GGijAdU3)n*nLM)F9SJAS-Z~wX2Sza|5uUkhnD}T)j;HXU~~Jq z3*b0FemD-0x>kWUAI@jzdDJMgbv9WFB#X&k^N;<;yn)8T!GK$xbu9NqrXV|igvE&$ zQo652Ir`VI#@4{flbSBzvIe|(l8{qGUyR)aX$Ut#h|!CYS~m{nVNh;4gC((Jn4Kzg zWt_=Tr()3aJaI9o`F^;;TOR7pj?C$k%5C%DfG1=+6og+j+K9KSSPriTXIVqTYbzqP zmOzsTw?YAp9)Hki6{18T3+Sbo;o+cxGpzxkPA zWN)LF7K_;>CXH?!9^E;;e4$Op9VA}&h_@phq@{-dZ|MNu{^GOOzj%<>wpzAEI*$Mb zu!8J?&Axx}&y+h#7$~G;t?ZLMgk{AQL{$aHivgpQMxuw7U+NF$lT$w>XFpk9LmH4> zOKc!`WvC@n4RMG4TqENBGO}y7l&G_>fuu}QZ8^eXb_8TrE6ppXLb^oZE0C};bY+Q` z-WjZ*L8C6JW1OY06vYI#3eFEPOz?DvjMY`X+th-FJ(b$-%$E5AmVU<``GS%`&bbpd z3CmVSWfhc#Qn~J&jiZRvhy-xaV-khyqfHKBhw31UayOW}^)Bb(WwFag3H`?wv%%Th zxDK=7JJMFtiC;E5&m1E#vY|A_Ms6PsZjpnF_&vNX+y@f#U!30k%iz`z4gP(4>tWqS zzmk;2EInM~<{PN3blh-`b1=}@B3yb+nn2E3^_Y)}A8BAvl1aLb7s+O;bg14p(WiZH z{m|aoDH0J~EbkrrtQfu&KC#pOXbz;5Mon3A`upbGl8@pCUN4F09Jw-U9<03BO;qX@ zFAPnSMZ-$9xn;I*xz>TD0nX}21ki-b7$2LtI-AzcVGyW5Xw>(bu;|^@u?l08H02wh z$3jg>8*V!$T07zG1asutJ`XTcYR_S4aZrqXv(T(p!-5@f91Z6$=+YS$99{sm>JA8- zX3_?E>rO7je*DUd#wOV!UAr$fPd7gs4H?Y&u!PrRAoA-=MjEYj=C0VxGEo93T`@Vs z&$Vw)U+jwnK~NW4*kDnT!GKEn9x@wJ=`sy9*^hFDf0RR7X=gIg&1M@=jAU0HFi+y_GiFbUDDslD!mZgH*TCJGg>|xf{10#U4OIJ z5zOTv!?N9x9HkNfZ^P;Lg6_Nfvm#@c`mGJC`c%%m!M!ZY&M?I)znqWzSUq-|5#i6B zaKZ+!x>28D?6zs2*{`~BT!o9|hvmAqB9ARz-ma0E_pm`Hj0t$c=f96#fWcf`g%Cuf zr{#^R0Bb*>7yY8ZWdT|0bn9t&i(yLUJ-ijj(O2Z*Y#uz)?8fbSKL#emNQjv|ck`%S zFT+Nrd;FU{|6*tQH+!})*Zr|QK+E*c_rMPt_YdZr@mF)EN&R8YVsj7XytLeSkUS_e zIdJudIUo8DwJpqIUzk{gAJ6!TWQiw+3qP21 z*Aas+=4syY%-fbFFvs$(;#`pzV@zJJL@d>VHBlA1-DPHU+dp6~yplcYyPAIGgoUTm z9fjtIo0S|cZlkMn9aM7E7t$D~C2( z2|}5PVM_rL9C>bkuF1J3xp}1{-bukzXcn*3uIaLjvRL#}eB6ol=#Jjw z!Pe^H1pTMJB|EElLZo+7_KAxhVFv13oF>`+uw1{-`HDqS92) zKugE*M=t))#454c8z_J7rExBN_SKzDxOKoI*H z=XWstnA6kvn&o0wX|i!clWwL|{T>>kjFq~_2X+-k^8LhAMEl9!cAGF}hYW=`xRo=7 zENfadtn?(Vg*99Bnf5zr)x&C4dC!&JRidZ7hRZsIQ__q!=Kq{JcSz-rv>Ozsz#Oz9u%KkL0O>1;*4mUn>s%9@x}J@Wl3Vv0~(jejI3;2o~m zY})-1N|c(~c8N~;^{6d^**G&BYd6?*+70ZlG~%)r32m-e;(lV zF~I9zd~)=Uy#8a(HCv(8e2gBc@!(A`n7mdD={dEKnIb~WlNZ?RC8#p*MXXJAxQ2|U z-3^kM>$_P+eK$~MR~--fPo~C{S3!IrgG#+u_P?kvBWWR4F~KLPcX*WGOk0L(L?Ect zSXZkK5uZSqjG%RG5z@k48nMD#rhFBL_9GC-Ps#&19J@wtsXDy)w555-n-+Tq#^bqC z5z)Do#u-wGbJKRn+2^;+wxyI0^&+8}uch6}O-eO#;cNeaR?U;U^7ZoTFDReV#!oYa zfZf`-dQd^3>gR6aJPq1el!Jbb%y0`#Rqdi>hc=B#kA0w?f^cOL_nQlH4|3^<6HU2=CX~_!ueO!8~WBwZ|JG78_$@A(+4(>rav_wrw=;~ z^cP#CJj1l|D*zPe;v#? zog1^`dYonpyy6(J@(3_Glb%5UVEufTmO8eTQs(D5y@ozkG2=lQ&M1(zeGX0xj9 zG}U2_9l9IJ#IczB_0^Gf9ZihUp%KxyM`Me%>N30&fR~yC^y$CUrH9D(!(*96%m+RU z?HydB@mpl`dYtOpse@5vd^#VsRRX;vdRa=?1n3aUp0;^g{7FACnNfr_y{N1cztY7Q zVjIcsQ7NKYOyVLEb|-b~Lkuw?Qc#X~yC;w)_5_^unB8xk#a3Gu@OOMneKvv_&)XG2 zZ{R-gf4N^emo|Uh;YvTv=qP4OOKgZpinBKKYKbJ$k<;zN*lm9~_B2aijtTw_vQpuM zO`lEqe#Aw>FpM)LyiSuBdKiei4wUu%J!Y5?L#j-G;(}}u|8Drvb!l;j@^tJ#EXXP9 z-ve8ox)GNfz{DKj`^6U050`IY_S4D#oO2XDzY7HQqmA5Gn)ek?PjgO4vJO8D?WG93 zScG5sc5&2rP7Yh>$pgL&?nfN}GR=1~O?An!OfKAj=I}*`LvZ)4{k^)`;?ioN2wXRn zK%TC85*xjiX2`6zc(2`UZo@(1CpG@%LVmydi+JDF(c2Y{^4 z!1s$8z{dH3BA&`ME08#ro5Io3)c$=pRQ`!gAj3ln8Jp65JKV{&>f$;Ez^+z@f zaw`n*My57U)&L^Mk(3T^RBN5DxK!);EvV9wsFfX4yEVr35EiM9eE7)(7vdAqqY-H9 z{nVO#sU*}vFjAYBsX=y=%3R)&bSqZ7AE}!0uN24QQwP8@+lf0L%_b`4e-jP`==_(F zThE4(o`Ho{$HLT9Pv?&VlS3~;ym{#HMO8GP#cUP3EfCX2*Pc0tWak3CLxE30^4H! z(aN`aa$5NZ$Wje_|Dtj52jSK~P2V+y2lfXty8&d(Y%SN?k@u72b@LeC`nmF&K*Brw zRS`3m_6A46h$!-{WMsV30GzG})-l|o@I8=yWs2`uDVC9F>C8EGcLn9NhiCE5wm>uD zxhQyFMYpY+t(fIg8iegQ72m6qi@`5-v<1ds9O_?}Cm0cLn#`2mB|6Rqvn5DXK7JCk zp7PPk6QH#S_ymCO7a`hj^gcj)*WgBAtv-^+Id8kCAf}jtW!Vm`f{)j}k#!IM2u|E< zw4-^Noo_d{vMbauSDHR#-%v!3vuM7Pk*!_%Ln`$MdSWJ#kmZGx)!-K-!W^V>r-ex) z-5648s`PF2qT6m&_}Iy%FEidUEwaW`p^{$$%YH%~Ps7yfFGD5iRVA%&9<5i^GZjh? z0SIdWKFNPgpn;w_Fk=eWpvMl*i(Iw~fKbK2C4;729Cxhv%CPmo;p@ofIrE2x zWbSnf7^woGvmB89xO%yd{c;vuqBnAxlZSP^)*mGpO5RiZ!2r2U!1s%jL=VpDH@QCY zSO1&&GW{@L+#lv^TzpWwDZ4}tII2I0?`#hLAyvn z$JEetfXitC-U^=uk~6K#`7%7Cp%k2XE5o)oOW@Y%CRaX2MJ|}2y@GvCdBby&<@=5f z^UKWbU{X}-myeM5*128MgYh;1-@mvO@f*>W))r<)zg<*z4f!!||9c!NSkfL>&w~P2 zCy|oo5KUOMelXvl_wVl(&hdn#Gty*)9a?Fv2}EXO?aKwKVI4HH#|(f-B=0lwafer0 zPkm|C`FzU@9gHm;<|Ags<#8XV4Az^W42hLVz)xS_x zYj$KpV-NH~2tM=@)RYOW%-GQOHC;XWDqbdW_#yG@lUj!M6Sj4|54W}!!;Vx?=EUgq z#TdQcV@S@vKp5M~&6vrj@AKYAO~t9m^%1oDv2&|#M7*3F-!Ei}8mJVGf{uMuYWxTd z7$}N81g8I+F-QDq%&&i^m=|jq{JJFt$4F{#8fJQ8H6msud| zOnpsvkGjOmWDY+R{-AjELB@jzje?uS1fL@mS}IkkC*zoXSg+t840#qXWV5?mIlzzs zA=k5WD+5Ai`EGt3-)-G;d;04_zGfmqapTp{CHhz%XwB}&sf*rI88bC~ zAqLN9=0#R?0T6e}ZxV;8g)1)Ng{b7wq(>gz7eIYo>KF;IpbPl^#hZv9w#`bbWo@nH zNNZ%SV```SJ1F_9Q?pitge=?_5F+1F9a)R^&Vr6ppk8sW85Ov0Mg};*3?1qb9i0U`|$qB62S-{2~(+g<(L0Es2~A^X~7t#F{c)_ zSbu8#-?7P9YimKhEHX@8ZZ`#_n9W;hxv%y$c@-z;f&mreTi(bir4l&ZQPpFqN#VOh zc*+!rKzS@SDUG#b!7b06vcdh>gO`*4Ff-pnk0Fun%~1Lv%7l>bhu+gcmsy1g>9n;% zhdHi|;g|3CHn$R9tn6bwte=&vi}RP$x5qlXZpKc|2;ta`pP-KAOx$7>DMO& zt)JF_V9WV@m($R9 zcvf4p)c{1RNSKcpK(v~JUnofv#>`eHL*Jnred%i!kKPU@kMnhTfidi+>=Ww)*pxJ3 zj@Nv=pO$zt((%6%DBC{@RH*d-SFp+Z=8);X5-8jMF3=FopTStKBEIV(VM_KVysJNw zQ(Lqd`~zo#12~h)SX!xz6%Vlp?HWvGi}(R?eQJI{T(0{x4~Q!SKwRynP2Be0=Tv#a zLPnEfzE^;c()!_}&>*co*Ypv&7ZvtT>MyX4VoQ-KKQ46=7sze~{6`AR2KXl1e^%f> z>BZ91#Q*Y*{4e@T_EUdR0sS@0<5GSx>o`{?ANV6zN_)taavpM}+Vda!3!p87sO=k3 zxmyrbTd^xWrE~|pdy1mni~`~CQ6U_2nv<4;S^e)-@iqOY!WGc5kNuJTXxe`jh}mc@ zw2l8a=de_i+D72 zy@x{<91h601NeTi@AyX`W}~$+)UwvoHPbV*u>Kp!{Jme1_b1&oI|2_NQyb}iJ`F>u zn#p3}A(fbUsC{^l)ff68>kIOtA|P$N0FvcO11~O9keUUjViy~}qp8YDo$o+O;Su#i%fE90XrtzQ zKEg~6>Kh-WY=TtI$!hPmEZB9WM}jF@jU;6gAZnipJtZ_E(Idjx>MriX>pE4}tPK(4 zq+wl<<40A?O&FD!2!P}s;QPhW?;m>3POD|(Xs+|PQt)@`W1dxE5(@CC#hZ4}hr7~q z1@hn6d(TWM10Gxp)0=z?2^z1|rp%yM)yhUd%hZYU)%p@D_cCECzqzElo2_gV+qC*22O$hPnZGD~bHm)rX3o8P3(3 zfMXeK-Y~dPoZLeVOvYFpK-yl=FW9AAs71~PA>PC(d%T^D8%f-DL)Y@Ic*Y#dE`b|# zLmTUS+wE44^M-aNH|uEp-LM&yx3jHBx$eVB$@IosBwpw>p#N zO+=`{7*ROQkLn^h2p&5RMS}kVXR`nAu;fpvk}{Si;_8z<_%WCNkGHoB%W~VkhXn~~ zk&^BX>5%S{?w0P5M!Gwsk?uC=?v@Twx>3485cuB@y7y6zp5J@z?e%_mF1|AFwbq?mwdZ2&%9#1RQLdN4o?U=`9W3dW;=uG^bJRFD{(g*67c*fRKosy zRPvv2MKC{f184XL5GMYO?1KP681@FDa*rdPQ%S5|0C@@tBt>}PtLDNIH~@rkf2Y0H zK(v>zRN`PNQJVpUAdaWh}Sl-#J)YRtFMo|!D9fPd>CiI4~P6bicK)-a1 zIcM|YhO&-SP{TMoo8zu~dc*Urfl%jRi$WJ##i!^q2j$GG$CoG{0S~YgL4Gp(jK-Qb zZ`HsWIdk@~bM{A}+k^nlQo_T;y0FW?vU?0A;P)vBChS>B7zDG9WE3hZ5Sq5-TfN0q5lNQ$MKyA zxS{I5`9&aIoAp|W!ZpXGvrhzs?wL?0zoUYy1&I5iz(jS^k0CyAfR=v zir!?R(w`lb3)4?nKu`$>V_ZMzw(bjo;qG=7Y!W8jyKBm4%Ar$2R}gZEMRg#TEXBCG z!+H_(0WF3rT_D8i*q-l&H?Pv_NwHn_Aj@Dh9K|glq92mW?*)YWCo~C;U*=<}Z$w8L zSw0TQz%veEK>&q!pZk9X- zdhecpf@qD*1CIx`u_k{YkEALF8W!Cc@1doF8mD*k>l>(b{h=~>x93J&yJd?}8L9;T zQ|XxF_vtwGoBc%Y1PKWRO$nZ%{@WPre& zUZ2bQi@*f57dtlRW*;oASjSYwg61dJsNc}Vv-^RrfE3YPH{}LOP&ZA)Eh$2Z5$*k- zO2-_(PsjfWR}Sigp+Dls;`88}Yzitq0d>A;WvSh`)VYa^Fy!K368c4()>NC|K=FwL z6`#9>UM{RjgOR8ck1~W4a&(N)Le}@w+kPdeJ+Wp^{x6P0$Ic$-Pc?}g|Eo#-D_*(; zCDxhM_(mnv{dt70slT+Q-g}Qe+~xqRYs79^B~L(W02sf3Y1on%?=%X z6$5)b`8mC-3Xn{}Dd#|}4ibpf$sP*hxgh~=-yfJ{<+&jN{zN+F_+NbzXZRExGGRuk zCS5VjitW2OkTMQ6GT6cl#J(v#dk5x=<| zF%&NTyFXNL8R_ZtOpO7i=T`;SFBV8y5H~AquG%vTnD{^`>s!QWK*RvTu-W5!0T4aB z8idm_&IY*;A2cjdjR*o=(Y2II>f*91{;(9FxjdnITGUt6aKtP>j9~vM3my<4bTzL{ zw>C>CltH{lRlLiZj8?gZo6&w!s|n-W0rbxu!Ku>Z^&hH0K)}CH#U(`lzf_`HmR;S1%wgXEp{3_z$#u z!0P^wb}v2xFx!R54l9Awz&sARe2OJcHWTCZUH4r%fQvmh@Wa>wk6aPs!~#SHM36it z4Fj}$4?(Q^kitjwn|5ym*5@4{GUQopFd3%-hE!3E&et)pQj@FcJyoAd7bdCzl+IqB*InWF0b&h=DoadM-LxLIY zfspy2#;X2#+{EPpKb+~O7I9aMMacC9+LfTrOU_jXk;tJ&azh~kS(GF%JtbvB3{mRF z&rpnO8($ZchocWNb-Gb2>S?aWs~nV3;exKvQMVxvJMgECn3A8dE)Ruq=LIS6-I9gb zHq6kyfUoce`d>zdU!jkRY$c~mB2;^szloe(D>>j~2R<;C8-^z4(oJBGCf_z;=OAPg z0@PqUxP}E94t~q9#!J~*W#{V^Fn4>O1#$&2$#Ro;>Q5uaFq>Hd%Z69RO_tQ=#Im|_{WRP7+@)BUw=awVl=x<6US_EQr^`=?tI_PJDNiqUtVOL zL)w3ehJRNgu+Md=pvrc`&4seh0RF6br|k`MChKI(Fe4P2=37#N*_u}GpQ=v`|EoTk zeTUFgAYh3Jr@VZA=&ePO8pz)Sgt;Pytt!LM)4*?2RP{JKDR>vsS9fwxfR zPvvNa|B<7|K;01tP6Hi3`p#eduf8Qme=-{?N3ajbkyhX1&1EV$3kV5x3*K>~QHux| z;>Z&ETbC+v4&ITw4WjrdH!`6nZP6-6@vw~+e<+W5bK25ZAV<@cr1DF%?bkQ~qCbEy zSD0d202Mqqg7@ds+m#v_0&`-C=VYjB4!H2XL$06NhaYydP=En&fX@T~ku0yO zX+2Fa4baFyeDmQ<3^8pXj~b!IDR6-4g`5ScTLsOYI40*+i0$n+vVD5~&2zXG>gLjv zy$vyqrufN+d9y&E-SCRMj5eAknIB%|yU(F-K*oeD(=<$E`M+G2`iccie~5r(Wc&pI z``th+_IJwqB>{_lL%?#oR=u1C2-xW{xv*uxatIKxw(B7P0b3lFcGyTfpZ(x0m^nl7 zRo{V65&Nr5Lj1~U5(PY~cf7$`acG_3@(soLcSD|hEiA0oNlkWmWf!l>cl)LTE6wcb zpDI9%zbZhapaKLq&3;S3@_-1~k=yNW;@XLzgT~@z4D`_mj8D20B#ootHWt*8T3cSh zu)W$<((Y7|DLSYCokX|*?QiwvuRY=K3YHxH+;SS)+Wzea|IttvEg=Fl`Jq1}=xW7p z2pYJNc!Qvckyga9H)N!Hj_(2pI{a_JIAtF@>W`G6mB~EXrWR z0RY|c3h?-Yam;q;Zzci-h-LcqVEBB@y?G1je>b;(+y5eYzEAm6 z^_KCs^|mMeJ6C1F`de~?)s{eJO4idx;1$wsa-$9)H+V9TfU6BcR8Onm%p;l2R0RTg zdf)U>F{iD2!X^XqA0{CGMV9IVe~E*=_^0OIZqzIbSUgD<`ppXnZMm(lo{5m>d!<+PjPV+<-jMHkjo$>8|&u6*+v z{Cj}7TdO^OOj%3)Tnh+TT-6YeP7Zm)r9kD@wl26k6(A!8Ctr~RWF*98{65;0rHNWeNQNHce7tK;PZr zRb++aX|ju?1H``=P{0$%!ZwL}SdZ<+zmgRXv(sF=ZN1DYLXR4|v45|m*Ew>Y_RlcQ z0SkPai^L@GqW)9uFXR8&{{DT6|5y8q4Ya@N3cqZBm40e}>G}>5=@eG(22dw;)lXmSgrFdfmFJ`x(IRx6@=a;{0f2~8eef|`*0%HGd``Z)uH}U*iY2m~hxTl10 zb5H4Z`wP;x2ceHZCXE=_#I3$5JNeAdbpeR(vAQv!_V*@J15^i)sR62kWg6tbg8GF6 zOS>cM;!o8jrk`NO&qsO7?f6FLjTxZ0`*G`=`v}l^0bte6pM~{-00IyeK7S}znn{tv zOpb`q!vP5kX7Ev*DuA%iH28^QR^tGd${Sz&uEc7VrnW3vAlmp!c2%-LA7Jw*e+w{` z48Ewl8=t+1h&_vnw^i23Qq5CIF=TmuPF*K4Px1Z~%wYNz%*e6&MqzRPeM_gQUKlVe z-~$tO`0>N`{Ir(D(kikSSNvw~1AMFt&7M#B3PdHt$xkrj2_(%xmKwlarIVI~0=4K% z`j@Iw04w*ZfUt6m=T`lIlY!5OUi9*zMyQp%^wyG3RJ@oC`28ges>szhGZ`9!jsYD=oY|RsP znuauo50Y-7)&Ft_(k&9_!sKwgpgVF5AecYq>;O`^j6f<^yOgcZ`-hy@vePUz zZihG5D<%|M%09Mv@ko9^LwmWi$-6E%sao$7jlZnjF$$U+g=k^jKW`uBsU!U|6S8RTZ>*Iy4l-fYoa#re=NzWW|SIt&Yf8P2`+!l=1D ztzYztd4h(e97hqY9NP!iret?LomhFzG+g^q35OYzz8E#_POBARuiy21#n0YiR;&7o zi-?PFRSV(s^qIG$j8qLjtAXI0=%|Z;;3`-Xg-j9sne_f{8x1dl~o<=U9A!s zx}VaLNwVIX$PybVNJD&p{O}a4bj^-65Mt%CXyg!l({tEpjLcB0j^4b;>Nt2h;>#B4 zg`m65IN&Ia?62!q;3dp4ub;8IOtx0nGzn41`3r0$nd(CdeDU#7X^KG|BhnErie2Q* zTxk}~O=X!?j5}@SBn)F%9iXO2VJg`ADxP)3!=jyYk{Rl*&YAeydr6hzm5^}>wm-6D zuRi7{?T2-BBG_jTv_`HI-o;nZNcFUF8i`oPnvV?0r}3E_OCRayiWKxLa9l3lr#lJH zjBX3WTlyl=!CI9ig*{}xE0>}dbUeU;eT0=dp{SGRK$D19qU$56+dZ5bMCs}n{ zgt=ev$lyJgi9B7X)L7-GYx>&x;UwD(OYZD2zkX81mMJw@s?zNC%?Q=5xY0mh+x=_H zmX)Ei{ZFO*X2j25_cFha_yMNge8(*?fMA{lvIw-xMPq#+fq29&^!Z~s%Z!#RW__Bp zUP=JGb&Y9=>|+&fPr#DLOn1uHBucu+WXk7~G4iS)WDH9EK)jtb`M^f!T6_z*7@-_B zCdiE|XRtP8S$?rGlV#Vc=7_v{>#T*UxEX@(SoZ+Ge|1sqTd*Me^#8ICgMt<#$S4JM zUItEpd-y?zr;|G~E68ZtV}}Zvin)l$IYbP2ea^Gd$}WRwsXL<I_OfL>(U3Gn*l zpStt0sq@<;DYl^6jK)aW_0Yv))cYW6#pSgwXbSDlHcb0sH@}UZt_Cz;wEx|F8Q6_ygT{OOYiZ&p++P?Y9DypID?1EA zY&}EIIC6oJDBlp9rqB&y69gf){Lfo{GpNs==0;ODED(|Mn};=mDhB)a3wfc1UM$Nc z*bhfAV5W-%zSLbY%{zrxQWva};YCKc?91pkUx0WassEZ4F#Ohx3$&R3S~H7rAP=6-a z-`NEI6RhZsNgA|om$U!gx1cc>AkVNPGhS&F?%k7;SOstL6jITGa8s-1LM!SN9;cfo znyJu>bTNlq0!x`bYvxm;5)D#d&Icy*K@8RYF`B)+ZbWGzx;WmE@*I%)P zYq11_j>~YS#fvU@-U#ib)h6$Yn`3HQ7YeK`-B;63j=Y&_fF$t?js$hE&L{wg_g`}) zH}U?|H@AX@dkF1bhCm`UirH3gXceHsK96J8GRzbQX4RnW&(n65ck9kIWVQevc)*0n z4w2RqQ^lN;8ih@^rApZeiWgQETOYr<$cieZ`4IVX1)}v)4)|J7xpo~bQl*3CK*W+ zmVsE++NuhMfnsaAHC<$?EPEixg$FW#Tv**0KtPvW}cB*CBA%RlQcKM`P{ zz5LDpK@|wP99{Z<+5HhP=Y}OiWU&-x*Ma*DIN$d&VDNEiksCs)P-*pmq^%?INL4ba zp}7{Sa*$Jk>-kKEcpC4r6lq!h1!jPT`BwxOM-mbm4cMJK^uWs`{}5gLF#J^58T6j! zkJC@xqxnO9L1VeDLcH;&1XOn8+UP0*{b%vaoOTH@6THX>F(omouiX`<)e(xI2@jiG zT$rdNvuVLDvHY+iKGJcj3)S#hIlvrF7BAGq1!xbW$;*u@u9=d!rb73RWG$IY&#}9p zr@JA{kBX0K?P%}IE?05t^fsd3OJVJ-Ir;NtjyQLPKC7AQV z1o7V0_RQs#=o2m!s>P`f4iHyKnA9?1^p?_^)S^{O+By{JuCqzHkX2gkb>;c1r4uiP zS{Cgcbl_vC?{O!Xvzt;{A$FMwNK-6mWe_Dvy5*Qadr}dX>QBRkch!hb+bp){5we&j zVXpYbK-rqx2Jg9DgI&y@MvI6u-p!|ew;zK^JTWx6s6Upiha3|sX>@hV3ESrO@gZpT zJvz{5i2#59L67xqPN<0W4|BpDfH~p76Fr@~m%yCxBH6JEWnlGnJIwJ(xSn8wRaMr2 z-V0>r_e<=XT{4%Lh-Nk63&o5Zt~>AoHH{KJtSy(O99Q+04?6ZC*~g+b9+%J_?<`-g zo*>}TY;v9-*mG#U95i; z=t(40|5Nu6I6B~aUn&awJ}ldlcvxfXEUWmEyKe!Xa#k zz+odAYU4vQn}xFG>fuy`&8jlTl-n*Qe^1ppF+sR(*fJ)?=1= z>@JrA@g9*2n8Or!f5f{A>BFZHMX`Ph#zxMimA3vP(_If3FD3#@Y@88T5gi5(Cy*2( zKIj|^D_H7b;vn$L*ub_R2_#`_;pt*d*kRi!>W`N9nVfL0!p|P4g(ruW5f_vSW6e_Y zULd;C&j{}+U>#o|%`h$Q(7{aINq)Olxim!*w2b{!A?6^mylOb`^-y&;T4ayEzMS~Q zEeEhPg*`IpJ<`~J{`G$tEGhDR34SrA-Qfklj2|fbd<_xj%IJl4dKUhXhKy{ZRrpbt zU;<*ExM{MqI6;qR^_x04xRYVQ=F_<+#zLkQ4hvYm!va;3PD#Rkh_J$KpaG`QoTo-GHllV$sl}*Qnkv!eUVfWsli{LE=hQg4Vg=kYIhNidY=Tc z1eZ$MICVz4J?pKD!`F>{7fwYz!G%+t{K~gt_9Er8tpT28lusm=Uxu#@a2Y}tMca;d z6N|125f0DsV|($|Dz8|?aaul#vpzQx-?C#qHF06a*Oh4>DmDJXo84GUiOA|Zbn`Aj*m~yE41T9xolD6`oG@t zmROUN@R!)KPnhc@&AWw|*6!*~*3t=0wygWzEX{9bZjOFVQ1A(awxtShh z-xXK|lGujGa}T~!G5+<)Xa*ImgePUVqlRbl`2MV65YtA!R_Vsur- z6;&eF&~;Ckm4c(JU=sUnSi*CmM{8uR6!PxS_H`T$Sk>jEr-{E#_PXZFX_|r!Z6ssP zJNMbeGo5uAi~GpCHQ&B@hT4vf{W4T@+BPrx;F@YE$Fak9*S?sOon4k&Q(V7rL3yM@ zGmHTTe%e>SJVyjJkau&!lIC>4*D~*{yT@vL3f(C9M2pn!i9ad}a+`7?d{T2bChY<(7B!>EE(s>jrO zX5VdmcI5jKY#pA?ba{(9IM-gXMFEyH7dU7CaKga%67v0s{XA#d{{^=-P}>8XJUYd3 zO-Rj7hLx3?^Nc9%ix_Lx1WJ7@KZ;1i9?XYIbp(c?LIs?<@5~+ITE~u~Z^;p+Wc279 z3J?oGyV7C@;iu{*uJz4j71q8fR9T`~3hhe~_t&5@@qb=IMv}U@DU8D1g5m&`Rschg z5Bc;Nd_6}gw4?#c#J$V;htB4Z<(ja@h)fHuSrh}P&TPstmtoRfXfKF7^BhtSC1j_~ z-1cF{z4xj!a_E(-`z5xaHQ64WCLuN2SDAPka5#zusBpDV`dsNNTvaA%>6h22ftSh` zG*-Z19HF^#&XMkENuJb)IFrxK8pvTRF1jVzOlpBf_don-_P(3CQ~3DHz6X9(4$1qd zfFjSI`3pau$Tbz<`!4|BU;N)3DNze+Jza|*4wRgf?CA&Dg6~zDr#Rh!h>9)6oJhwp%J7S`jL)@<@-~acMArt&Z#0*R_XVgY z@!#%9T$`sQmc9?3K*meNjxw|0>#u4zWkC;uwC$x4r~xW`p&glq!;G_{Kd1OC&V|Xx zy*yEs&&dEVv)9`_$6+#qOGyHBEZ5@a*JmMEH0OBLF|eBV`~%$(7sF7C>Af=u z6*(Yhjp8(Wo~5*?hh}NWm<3{>V>`*5`JazbengqEhdJh+&nJm^-SRnZHFGlY%|Q3X zR#kT<l?k3y#F|!qAi@HNeyFor3DA0m2&YKw~3DuGEayPHIre56y(YwE4cXdE^Q!D7c6JBQEbNA*~ zcZuMKAtnVpI9XL85B6a{Mum4aMJHgo$8gW+3dR$0)W^E<2|t@k6xrBcrZli15z1y{ zB*;m|#M7`AlXIiDA|6PFCt-s^y1&2*ys<<+c3fDRSXKKKSEnRJT4oy4PEj*@!hlz$ ztP}ealb9&SI|h5IIvXnmcZNb~>0WDwtB-tvus->pL?0Lzdt_IPfo)AMp(ApC2d=cngH~WW`FXePL-eNI_VV#6-*9u0GRg0F zY1|VeiZecQCYX8=$pU4vuDUvmN+jq^NiB|9i;s}mGKng_#Xs%Ca!D32?O1QwmbP3`0qr* zz9;wo^Z_r%mPSk$@iXEKQX3aoy#M~&1bs4p{)OU7!7MN<+@cvo^~ubs%?Q#Ram@1JY)jsBY4g7foMZyh&%<;zH^UVDWp-$avy3khpMC z6Lh!6#=>}`5Tl#z>2aUI8C6jToJYgZ4m5=v9tUWl+k$`NxMRW2}tCV5KW4axBE0$mL>N?pEa)Tvv# z_=>b}3Lew8$!_SGn@euovxRspT}VTQM-smG%vB#M@qc{?A&D@#gIBsRYPg-i9(mSP z>w`&Gvl2ngLF#BNRj!{oFCVbLBYD5UpAfBs;He&mafVuw`^4Qa-bY_u zX*P1F$-W5rAe#H;7Km!aO(SNpS!y*dz#+2 z-z&=UHs=K?pC!OP<^E?M>=DD$S-jKw#45+B_X_PT2I@`43kVTvD7?0 zs;baSo5aTS5urjPn;3U7Jm1=(3-3e1o`u9U+Y2+=~(n`T%7Vs>}xx@ZR{_ zVo+!V1>impzMA!r;TXMYgUx41+oL~9ud(y$i#ghDT9#jHsJYzqsAazDpTVA!5u>8TaMH2^7-O%Rh$8nu~2ux z%Q}iqW&^ih#pG$7l2@z7d$u)nyGEJFD~ss$)yF2WTq3wOJzYf+H{0Fnp=fN@JbqHDE5QuDIfYFpuqZ%DcX@-9Dhb?w)u}n4INacR5`P#i2Kr z5m3>N0CRTYm2LJ+5jwjAN|c3NVxfRG$ku-FzQhICQVDE_R37_mxla`Nh71oLg&x#d z-~Q^pxG;*XdaJ`HNo|oYpHRp&RV2|?A;A&$B28!uKckn_ldG&TT?84vl{?Zv7(pUi z961akCFXw8>L)zLel+)<{!4HPH@m0L+8|2;L*EPR{4F^_U$buaYlyT3T$xBi(OhUO zH=Nc3i>Fe{Wt|}|@Hy*=5@I#0jN#Hsln{>`!_9`tQ$LRWkb>KRrRlo=bM;f14Md^oGcTo5CcQk*D{QeNz8(T)mO~;N=KGnI0^mcSm`t>)dNx1o0Tr{*Vng&O;`G2J{)$uIdE@m((A>~&U(b?=i^@A zd|{H3$27@B&eOAZ3+Gjc!c&5#o#_Ia{9;EnYnXV-$V~Pvdr{Z{kta9s*N&{ji3;H%-q(CK)8{B$Rg+BB!G#1X)rC`L zrTzdRuYsUB7_u-vLlJCH}6r}z22Pa6H0AG$t{CRH9R9rSWR&bi)eR-F}2EH_8<-S zN`uAXn&|hq^=V*}BKnOL#FwAJ#7$9di$wiqz4~~AxP5n zJ$ce6u2i5TM%_20Bs)3QO;l_(5C=Q9m820Bla!*i6q5uI^S+hrla6e3Qo2fsuK^>v zQV*H_Gq`9*cXvborLUt|D~j_SK0f^oN5cg8)BO#x{AD}f{m($48>5;b-M2AMgmzhckE3}x3D;4iOBCZ_fw3aJ!`S+} zyyHl?JWVbgQ7Hs|MpPsR*oh3N!GP%icvfV+0spbNitIZna6DYQLz%-Ga}1Pl<(U7v zoiXDO<+BL{1`llKfB0O~x9u#YYXgj2{F(o% z11x2*RjW+$z@QxMEg?PvJg$aQ?u)s?_gL$#=`?MaMYbx+h{(^+9P%Jf(BY${)tdRu zVo%f*RnrQR4JgAq1x%jtvE^|Lt({*E3+XHb*MaZ)D9a8jKXp)VMy~)Xkz;Kmgl9uW zZBsAtbx67siRU0S<}{=qUrn4GvAL+E*1vMExsAhnQ7pT+Ui$emo~g2jYk3xG9pP?P zh1THVKvwSw7?U7f{U1(QTX)b z7D_&UOZavh_^P`A0{K_GmXx)DgT)VOiDlFP=m9Rj-RB|xc;i0Q^UGDS0#-1wQ`~Vl ziZ1%VQ`aR1V)r~w&&fYXXyMu?ei12Fn`M^-Yx_v3nbA}2!XGr;`oiMwR|onAqU|=~ zbM@q?2-u;-Ch@ht$1{?|)HxiqLRg#Ef#}bDFg9Qt#G8GfEFsa?kIjAG`;r!^KM?SA zMRL`sVv!!(M;(s!z&vJN7h(cX8T!6-712i$luK|7s7tx`XMClw@jLG0PIl#S5bEnu zBBm&wi=mNFyd-;C8pns-I4mB{J*wS%_S@KOpBRCcf)^|$i}2S{SSQxIcR2KfD19)( z?VEpY6*rlK65W9BIRbPZ%D}h%2XfyJ?2N3gEg(Yy58>Qu6a8aaWq`@-UAq_5+Dp0; zHub((!h0lLJ>$OnvhSgpKToYyhTdQCti}#wz|;+N{J49e-P~A3#PkxUCXQcKkP%br zfq)-EznJm;IpASflCXLaYJb~jUr4v0^)@xz?;8uikXK;QYiMNGz)aDKzqLuuy))Ow z5ElkKGD6$HsE7GDM;*GxzM$q3V>wTaa*DJ5B0UT*=x9jc)u;fAI+lQWXh?HqPF6c3 zlpVQEJx3B3BPF@stHgIOGUIzxr6t*14lny0E7}hos_qoaO0oBaX&^A`q`uSKU#8Hv zhZ{KMsLQaWL^|iV9|FhXxgXx->u~2Pm^0Nj0MU)E0T<@$oxCZ8g<}y|xDo=yqyFYE zHm}e{>KUJLpfJ9Q(~Qf)12~Bu#byK3*#XJbfyVb?U-|#IuRjXYV&1xQgTW4oZ{SQPQLx0EbhAjhd6lB{!@V2`znKDfZDJc~n#|6aqe@5nGFSza-KnS9~iGXyWvf&~W@rm-Gv5w_4A4;$z^citP^gx0an%ulaXY zg*p)NE=KwFzht$*>gqV#ZgH)JzB3Uo*X&)oLsHeetwMHKWX1GY)b-H}K<2gKCa6v@ zGw~inh!r{(%T+-*j~S_DT#_9-zKnHHqz^>#x62Mwi!=yq+<}#eKB=OsuYVNC==|~| z3tdOQIbkQfsEfy26RplhPLL&P3C>oyq;A@)z1CqK2hsbJE6t?XB3Acvf&pU6a08^b zR#!*z73u^Pr_5Z?p9;OtT9dI8c<?Q$J+v98YZtyd~+!5f(7n{zAkLGDqFe#%-1LNr{Wq(8xVH%gNU` zr<`sBb`Ogi9O2quJ%ghy=gi{>?RdE=SS!*6)8%6d5i1N;otPzpxoq4@M!$Q9ySnE= zjaRkzb=~9)k$V$d(ZIy$#bB1;$1G{JFoy#EcsM+}3*myGV8#MPn3JNZcI@-5i|`b+ z5CUvh{MsiqDKl?Mm`4(|Pd3ineP*>gg6^51+I9eySSFpK@*@y0sy^qE7X;n#=etq{M zM)Y+q4s<$!64q8MHmn7W-QYT(2PTVje+GS`mlD133qc<_h9udL*axLqZ;!OQw&KIy z3W*=Mw#VSz7e(Gcf1XMPS*0LqL=#KxURYsc=p2WE>|oF76QgSVd061#xP4Mm#a8dT zr_D7i69}fv;5?JXI)Tj_;mQy6i{WtY$|M}|L_2E**A=DJN4!S7HzITKWY5!&pH{cS zu@6?a*Jr%Kcn8%N6`PVYs#jTrcipfM|r=c@q5@D{aS7bOq_6{#~Et>kNi#rqypL}wl$IlSA#HChFaB%-iAa~sFoQtwGey56aAJzO|j zm)>)|^Od07myb*8BEvZiF<`&axr95!wAx^#mHb-NSlgWFgQBSAf&2 z`>=Vnw6TSAmy7MJGNhQ{m-PByZl!xxFD~c^fbvI1`^P}^S7-0rIHgooN$X|S`%M=r z_v-^4z+byN$!5%TP?iP7JYCI!M0d@|ecK*Sq!cZoXK^^sS%&xCoT8eegLOSQ@^Hz9 z1ett_eeDG1P5PFy1(^jdjc+B`(_IU5`+lCwm*SQtLQ6cpaRRj;byvqa#hAx^WywV= zKieCX`+Ix{p>DdXAUZk8DKY;P4odMJ+cP3ziWzHp>l&_&Au1u!Q*qVBidvbJ;lB5Z zu&r{LZ7065cgt4^aY_rt76-gTEL>lw69;&DPE|va%p4ns|LiBH?*J(#}_=LDI_io>Siy^QSC(=?(Eu7Z($DhH9d0`>5!y2{9AdbF)0 zG8p;M48OFsm#BI#cX?$>KX%ePZ^Fl)pI{MI=HpThgK%nb&1jcSsN~y z`h=o25i_3!WFkzvO-O2Ang6Q@>Yglh?Iu#Fy#!?wg~+dj`Meib`fYkm^A!@$rQbi_ zS@eS;)r{K=5EXu)mKCG8l%P4Htxu^B+_d@l<;!ABYWbZSnDo{%+V^0_k7n*Qy=Xy= z*X`gjz{j^?TTAoK75C}1>&Z6M6b=(r6-psR9~JYY(}(#|i#+Hv!n|`4O?Q!Re{aJAI*d?|c=BDtw1CA3aigt79aZsf8BUS=_F>_$sd4cPnvFxd3}+ za%!f4^;DO^#>Bu#Ar+JKbXsDGR9Y$Vh4__jtfSk1!-zug%d@cB4N9#S5FUl{U~ z8dXS;*}3Ieby~qIU;o^t5m}26aQIi9@l2XtLQ~THbiz|(LyyC_ANuTsjxXvcwN?ke zln(*7&c%ZCu3M2GMQ6(;@rO&_-^!om%puE)P3F$7o`u&KHj@yP3e6gEHZMel@(qAm zJS&eXYs$ya&`iX`rc6m-#rDeLA7u^6u&C1Uw9BJ3bFT~3s|h9U5v4Dfv#i05r!(*h zdo>hu(f*9YOaX^9)+|K@>a#2~-+gz4D^+%!c(%gLl<~SS<>n54@ZFz1KoAS+8MS%`^OA5#+Me1KqG;_7 zxE=KCBOU}o{=F!qluC6NcQ_r7!YtDndMDDF`@YYX)8eO-@%zua{g;JxTIWRflpCrzn`~bmHKe#UKO8W^ zPnsx%(d~?Zu&(a5u5QM8F=n!{#k|>FMuYXwu^X`Wk)Us@T6GlzwnODDVSX*Zqcd zw0QQTcrJns$Z;Z)1-=k=>~1e5@+v)1)VfPfQ?ITQD_cn;W245C60?%eT!%W!WyJOG zMYgDQ1JY4kH=ndCyD)ecGb$io2e|IPI3T}&$l_#Gobzf|eHCqTL&H&wS2F8T?DE09 z-D?OADz$F9ywX75r1;HQF9MXTFs>@7flDM^tND=f`L(B5l8r3QFL^aBc1cM(QVjVo<~RnUO#)4$mCAGQrl=YpPs%msn7D-ZucNy8vkcR z$cyK17!54UOXPq2Am=xF8Ob!(mLRm}z$!Iaf4 zbq_-D5;a5ncveM1Mh9nmSH21zF+Vz!h;4&af|~tkq3t*3-R9b~^fG~?c}iJ+rR|DP zsmq|GR0EkCX*4ypM`+Z&G1kJpA)G8;_!X5$yF}I6W95h;{cijubeb5h_(tj*f<1)g-@wH>KaX;x4SH(MR{q&^at;I)yRb!`hJls$y4D z=M^{ll&#H+BHS>D*BQ2;x)ZhscRT>&DVjx1NydU&qjsn*LP>{$6^)0r#o%oIT8S*_ z#wA$@s{ThKO{9iLY&hDF;$`*lY?_h3K-nr*tOSEI#P*fMc_>X_GTO{YiJ(a&ON2kx zQ!mFYKwH5kgtMOR-$Q;(h06BOzjNG9O`qO8Jop$QGNjG429Cb4IY=fQy7g=0qGP*5 zEYU&&>%Ijv&BDfZwS=Ai-t@C8mAl*+La<5l(d7qEX;2!p`BXz@q+9s!oSAjn74^Jz zBJ6L&WZ&9We<;becZufsY5El=&%>iun){q2I%;idT@19@E4c3ssEpj!z(!wE)dmuW zyr8|ltKePLJQ9gpQxy^^ZQLkJT}ne1sJ>!VIOi&3iZPl$gy0_#OX-vv!%xR%k8X6 zi+=E^dWcWfT{W{*T8nk?iI2!FVwpcNFV87A+gX@z6IQP{PbEcxb*rqCysWE$SDd2sJ z-roDsvQ~?jl1&MuZbR8Id&y)R>OM|OW6SQbG@{^m5dX0N3jLs%VhXB- z2UwwM@WBL}JQ$U>5C?3jGEx1p*I2}5nhDNE&n`D5H**DB-@Ghs1W#({YNMM7v>6*~ z5>{+d$oe#YmwK;h_LWYBbbmb>?WQ1-(1NmwgztLl)kA@!yAZFlxFzr~>3Z3-QL44_ zXC>Q}ytk04y^ju^Qe+>Rel^+}a;^O$#yR_$zxb{lvEwU9AFtl_HFUaqk*#N;nayrF zWDM>!(>CYpOX=)S-4IYV4-BU~xn&l#_3ue;CKzO5l&fl{)vENtly16o%0#XP6nV?AD2qRdXuAXm_ z&dg#JsY6yHibHps-Z6eSB{+41`FUpxJ=mGR@l`(ahi#7xzlmy`su0XeX@mFX&t~kw zYerSasN_?+u{5MY&M+lI*k~L)kBIc&vAodGL{{12?B9>*Y1U{j6Odto@Z^--C(q*S zy;jW%zaRG0!_+~-zKke}G%!W|tk9poG-y{w^XN(8Jed#|uc>mBoUh}(6Ty(505J*pEe>iN2t;T{ zFnpQ?b{h~ybI&NuC_LWOcseiBqZ=Ea@IDsY?Tg{{Ly0Mt zblkum^TpE06~oHfoL3L+z7!-p*wE(~GiHQ$v zRd8?Ly1Eg50=&)c6D0<`uE!_+6kp6U$#+@cXhc$QiK-HA6neF9JHiF)CCVn--lGx8 zVUE#hTWDm>N6PdCgK1>(a~eG+5qxdQLQ;jI2klpvtdwc1zf+!0U(M>qEVL^e&hFx8 zP8g+Nb;NOnp!H}kDuHXPTT`{@E2<@YZ2JQp_R zBP62DgYBSEADxfQ_``QjEonx;`6}n ztXDa$A>|h`*L5*;){F3OVC4pz^G-laYSY_n=MqnDm8T0BB9INM6+es5E_@obC&o75 zZV>g<*r@hSsvJTT)A>D|E`B8RcuKw~Qt58;ZZEqTy*utR7Fe`_yh2=TkuMt)=8|1! zb{=!X-hFNA#ZZpoLU#U`i=gaNlh~7c#HJyP@}y$VDiA5;82N&hKBdBM z&vCxUnG0rrPOjk5P~byyEZtFf%6#f=5fnAx+5a@Y9^U*C32ArowL-JD$(o__=FIEB z)`G8Y92zv*UpQvRgAQhS)346CS*SJyinCg`Z;_K>u(gTzfIh((;Kcvo+wea?QIJq~ zBSmUOOIfuO-1k4_fwZme}*1eD3 zf>zGQH$?pL8B^<(6S(c?dmhk+XKZRQJRw&M0)t%Fu=F9|Xm)gM1nOr*{J|=;jRq1| z-lc@k;Su9PC*rrc4x@P!z~ucyqvoFISF}KhFrh&sebk8c6-9~c%Jot^j0$1ihGXj&HsB z@?poqtXK7yTNiLHAj4ky0*Su@`29mE8|0U<)3-IXu{W`{`{jcJ-)|>h{=eGJ1gxg- z>*E3NMtHQs0?}6 zt}geSvrqo-Irn#;=RE!2=XrlV`@8nuYp*@6{nGC)dgA#cdcsMsv*x>$uuT)B$LF@B z!qu25`_5xZ_)s0W_Ybqo#FQtQT%MtM|MBT8KLvOJcHPi0J<$Ts9eV3(XWg+Gw_tS7 z`(FCD!nr8*a;_wFD+>6fsi{;sGstHd0f(q85o`;?uO3Yq-!$>kgIjiO5P zt}c7=^?+wlx9-b6iW-exe{olL*rqgEBtD`cN^f$%8~2S5=cUC?Pv{nyJhrgD@xXyc zPFru!TA0_o!ei&C(47`%qIUI}=%J81<-q)bg)#?b+#e(R1$KU)uK8Lsad5+iBfH%0 zTuV!w81Sa3Z~%ifB0@ky=;v7dE_bH)z? z0^&u$pmU(3GQ2v+w^36Jf~!37m0?-8x3`4kL-I$2KRu&@Owa@zCqi zhlFPX<)jamD@@q=*!J_7-JKqu>vY6*bew8K$tI7PGTrKp(+AubYO-$KdBd)T>y9?a zZ-4Ve?5jvvVMo7C1KwCBJRYQDWA?mk`%KF#d#t>MX`4*s%+X(SBkx2HvsW%A9qhB_ zzE)YcH?OoyUFw3S$D*GF~Y(xp)4Z`sw~ zp+ye|d>7^PPcC6H8Xs_Z~IAGdJP=s$r`hb?Dm9V}V|GxzpC8G>$9@ z=Gx4x9ITmRY+8HvTtsom9s6@KKP!iIpW%ORTdI`Ps$}ynO)j11opb%AIN9LQmfIr^ zO_)5oi=C5c>XiP!Mm!#?cqLF)ai7bg={-5J@ef9y6@4C}^zC*;gWTk_=#`qA=A2le zRd*vN#{NX(-1mR{W~r?7`i!whR>eh0AFw<-V!3sU#=>Ko8I!`Y^R8U^ESC1> z_*q*g#W%GVf0RuyP;!0laI@aLZ zL?rdyqE@f!x@6q-nfB&BWnkag&6-E7Mk<|+S{w58fTrTqTc-7Lvf^B&d?*CR-{L)$Zj&SI6KVbu3 zVYR(Wj%Eqx8r)pG9yaQn%~ZeK4w@2foio=^>#FuEhw7N7u|5<0tW@eUZ7x(Vja1=$ zTxD~;LNCfMj7hKa##ce%4{}4U4=JI#% zpqN7Cy(W%sUvdU5Sgq*!+4W1*qEVhRGdg*i434r&5bYd|+=pm!;LfL|mmHrLy#6>% z`0>&^w|;pAEcLUwdSU&G?2DhgruU3LE%Zb|^~IFwcQWSapN<+D^>M)N^qSM3)+ocN z6_fa=P4T@pXaCADn)v%zwww`a$X#Tm-(9~qSKqiq=Yaaz5!$m+L;kJtRcF+|{RhS> zpGnCW_*m52qTAOmi&%aGM z_hxHTKAaC@VU2N4_Ro+(dq*6O>>}~ubV$etYxmgtXO?}RcVF_==57xukq4T$J1mvy zHX=R0S!UntN76H$-1hWz9~pZgr@U&VUvQw~ym!C1wVc`MSR^_}Z}*Xo?{1%{fTpbX zP;9N_@?DaF-l?ZAcxuaNyUv>YTwF*=GbsbTOLj>o3GffbU|NRH+wzZx5V(@Y!H9VNOH{dgLf*y1 z$n@=f-bT4nuRbuo!x^9-A{-&|NpyF_qIA*H^tW42*iA6n7;ln#pecE;U-NRCjU(318;}#M zyZqZA?{&RCUix}v@Fxk?C2I4UJO)pA+}wP*yBb(OZKZm8uf|I`BY!o0{+XNcXshel zg5(N+V#9uPGr#y3H0<7BeOA*3^3gsm{^+G7HSJoHG=uQ? zt4T{5hsHi09_u;umPYZQ)c1NB2MRup4I3$Zr04nduGS=MpG4qhzsUsrIuM96~&7Sl5s_~jCiA6CxG_MT!c4xHz zn^gC*hhuHF>b@8>Ym?gTqNj^Dz3}a*lQg?{{oai872C#o4WE)GtrBtg+OCPGIvx?< zcQ7{a(feZUryq=_ZZ0Xv$|PUbmk;ii*9h%9Q*sckzNm9yf; za)rH@f+9W)uN*MUCC_hwkb=T?>QEzbr5P`Jr^zqeYP~KbiKc74?4pRnsRx zdGduP+c#{SB!5`-E?%e&WsNCss_m!On`4(EAJzI8Z z&+CD+cGMl&+Ifli7CqbGw0xsPu7tjA>@b;4Ar~g@xLfsnPRWsdVI6PY?$vKa|7kIk zOrsZcoEI3M@m$`v_wlNGk!ea<@c;B{_|i{iMtYV-pZ9sqEqSv!)?}(tZ`Bj>r3bu~ z6NjzX7uLV7d;eRT^t3d@x>_}6T0b6n-}g$=H0`BU!V^{PwKT-_;-)v&D0S2*Gr43c zer%h_pv*5EfSH8h+-cU8_;QnleCm)sy{SDh}P>zI}3r*~04l zmFuIY%aPQ~yyuL9S2c%y~1d-%TT(1v^s@y||F? z_4b5*><Xn);N1tW>^!>F#W$?l^6;ujcv)n1XO$sQ z=a+67SwG;^k%Vf4A@enErUgk)_kLKUv+Ygg%;c=Y58PwQ_75zqoO{c!X;fd&vK0%| z+ zJP=K>^q+mdSZj0tM{w`7UcqpB<}%Uunpv~vWG2PkGL=1Lon%+!((i8Nlj{ekN>&wz z1dEGwj4kOfx1fGu<&jv~93jS-=y68$y*ws)F88b zjGXGBkNZDIzUBPmUM87b&DkI^X4Zg}F4>v7hu4(uUbDa{ zI;m4^{lYxi0wK2%G1`}Y99)=@v0?h$;m0hwYZs)3Z=4cd5piPLjH_PaM>gLR?Xq*( zz@BQ#Cp+Y8y{%NOkBKR(_SMMU|d^!O{J7Kq51Hnw>&)(XijzW%nlEh?FUL{75V*Y<}WHyWL%_dw%Y|;jX^( zVXpH-vDLq&JboG;pOj{H_mO@>#knJ0Qg(ScKg`XSt-oipA#p&2ncv&S=(j%(U+!^0 z`SOafCugdD(+jgJ?>1m(V(*3{PI>c(PSG|Tp4>lDVyxN}??z+oY2(!!b~SH5SL%CX ztcZH!fUoOHgJUv1bVX8Dcm1U}Exh;7>zN6OYDzgPv&W3f7jfR!d&;E-2kTm)Bclgc zT)SW%e{75C!i(O;S++MzDmGnv;?R57a`pSd*CdW^@L6PSSE=xBcxF(n>z1;;GuQY| zpQ}=5{A`bCvj0Yd58JCNH-=ZpoZeI66&T*h*XY!g;ztqBhg{L{{wFO!p ziQ_JpC}sYB;u;ZjKl$3wwaX4Q$=%T`>ririX!r61AMT5z2}6a<=WU%sFI1O|{Jzm_ zbc1qO(#@j*D>Z#C&ye-@%iPZ0HAN(Oq`8=5!U);v6SFNQ=og(@YkPTw&g3+Yl@%)u zf2^>YH+W$5e5X&gGtC_3^+d`>op~L&QriQqIhm(Ej+?>5fH z$fL4K(bBltQTOf1$r=|84qLizew6ChrP}ITRP3GdE7xCaIFeg5psG9`W(~iZQibo? zxw(ajRQlOHPYl|4wWh4zMQg8^Vx;t`AksXo>RUtI5U_bT*nB!OoAZtQ{G)!v%(E7d z5aHAZTeFf5#`m5qRTtSk{?iU;p;KFe&y;5RPVRJ8_II69kZtlxt%+~8h51*0Nl6Of zaEi4`dMvzs@y%FS$X;qaa-U1Qm)`})$syPfakYCQB`ppdvrTzS9N-W|>&RbFa~e;UvGp0jgh ztjY<;en+1e-}+r3pD^V^hvQ3TG!8qZ9^9lIb~@HK<@(nAq%XxJm;GGRVG~rtNI1G{ zA*gEjbQ zDRV>b%uMXI{o}~yO-HZ8=Jp2DAk%&WhAw`#qMn-`o2$5T$V0E)8#nvAkJ4Uo=3D92 zhVoM%f2@-5koGPdvTfvm%^Mtig1F}$G~8!A2+LFGJ$-n`NIL^D{hlAyO7(u}O`9_9 z*k`>%!cM{QX|ej-t&Nn7($fuwxVx&RytmzYri;%sf6>X@d-5LhE_ojQm121*x~#*p z8Ux9}c6%kWF2I@Twj z`V!eV?Xli1MdeQ7%P$UXe*WuiuEL_g$s67b*z0~@MJxSI*v#3sk`6;gESIVb%oy@i zr&z1!=-DQD?^pdMtv{Xpkg<*fRh|!@0kf({>+~UR&LRE*-n_>*H=IKE@m4lyU*JrY zHVJ7<3JbThz9h;l{A?aevdqH)I_3wwQ@vC2o9;|b1 zB0X4Va^SlIdV+)HvxI*&dbZrUZ{3>+Y`u>b+mhFLTH8xdXk!9B^;HK?UB7~-zP#Y= zI;}`iHMQI5v9C!nk=p&9cinad-dkDo_$+#FWh2V!FFYz5y=Pvr7qY~DxLq_Ay|gftu#kjfRjNceIyh#0{;r!$!?4A2|H6 zQ=j9*Eg!{I*bjX-`El;7w<=Sf$;#_{nk{KEb8~q1HA3RGCe-(cJ|aO=6M9|A=>5{* z`VNEbBd!*ReZBZ*dwD**o77Pw{lbe!6E^)k;@ekeU(QY5s&4cnko$)YDjXF4LOQYXD z*E!Lz&ByjvC`{|Vaqc98a+#N77iaZ3xyZ!5m%sEbvu|NWmrwR-6PAcRou{im{o|5SnepXvs|oW8ZY zDY^yvK(b<3e55ak65(I-{2%_cmVI4C-`2w>fBx-`LZU4q2WvUjuYyKo0tZ@D`TlJ# z=kWjg-!?OuYG7ey&|dLX_z<;v8(>T(*b!q4V)0!)1OM8Gr{U-9HS0>?pW4~IHKhZO z!R0(*1^`?6goV2)c?I&c@K7n{vxRQKzpo1s#S|U~{&G3ryD-w?6d33h5X479|0ucj zsq>L)_2l3J5(BFtH%FG!=SiZjA4ur|c0wa0@5kjVWe#z63UpI+@%LNk>HZI08ay8S zt<^K@#z=SKg1yu?Anojy2T@w(F<*I4ud}Urhh2m)99Q z&fA)P0Wj6Zw|X!tLu*yG;K?@TF?(e$cm54bd5Z2al5Chh&=Czi-F#e4Py#k5AJ1^N zw!FVo-K(nfGdqIqji3xD2^sIl<)p(&aF(`51tD$5QA-f;DYgUJkfK^%su!aYv{@d( z=`7!A*VmnB^Sm3VMmsgUAD80}Vn)d-D=FxDWTA^xiB7}qpiFjmt#4c5giRWu5K>=0Zu{w0fBtO0k1!gwQU-Kl6*RV zUQ-?p{&G3;BLzY9adUTa3B!Qb`@PEY2T&klvA94KseY|6C@H_R5+NLA<8Xq z!npCON-q9BK5i|k(lK`3BH<$w31ANZ6Qei>aMWzU0l2C>%(3Vj0LVSih|%IE@5kln zTd_sDyZO1b_zR5Gp=%GP4FhIZn4OvMpzxQ=83gOv?4-8SimVUs$+-n`ZG|DyBVaS( z92q+-wISsy?DsMP+5`Ad@+;nt%ef0j2HDYg55!QZ`-`e3n*s3~e8k=vIadJGP@EIK zEcD+W?4n;6sZiydRfS;mDTS_C^cUyl*GIsObSM z*HwfZKCNCLAZUo2kB_&Xe~2GO^8k%0%4lS_1|Nae$#_35r=tryyjt56aGriHK1*D2 z&f;eAisu`Ejb2G1`lnZ}Y}qX@Sy8S1I%eI*8GuA-FLilZ;V*Qdg=~>j+Jaw2aqF=H zqdeHr)LY+Hr6Lu_fBmWl$cyk1ZMxWtEwbgoEGlyOJmJ|1fIAA+NVI7mAGSy;&BT0> z1ab}3PAcrjmgev5h4&1@-9~8b1E>>7MEm=^AD5E@u_7DKt4@60nyNQX?Bi2((;H3c zh)(J3V%F%^*QKdYvywEiB>?mWy=FYI@R!SRhParWUbL?1>J$**6o!lY=Pa0c871<5 z03DPVi0Un2N1mkQgkEWEFN(-se)(oRz$b!iqBE3-6oqVLx53*9C8j7x$l-m3To_w6 zjWaA7q)?6)EUCVt_fob<8ecKH!`l2rK$cN_#ZkzV$j-vlW;}F~*l``8U4xGpFlE7u zknCvm0n>$-H>73)aS$|NVvjXn$(9=6=I$1{2s4g*?lpd64p0ez5~H+Ek?f!vZR0rA zys^HgZShUEFp4dcrpfkMenA}HWOuD*i==6?Hrc8cYr(Q>DNWX3En6hjrs}s+d$|M6 zjM8|PlzgO6WJnZy5{5R6+f>7vKBTebqQh0!6ddwYBhGA zxo4yu(8g1`=A$iaX>eN)uIgbL?80=puk!)w22kPk?|(vU8c-j@M~rvp zZDUIf^l%DrbMvPt(pjVF;@#=9&aFg&-)n#p_b_f{u!gq! zqV^`?O8Gq&;d|??9qjPq>E?+FzG5p-Xhe#6yNS(NBk<{Kg zC&GRKaz`4Jjx%`=TN-`G@#>d8T@$F(L9KZ&TPp2HIyozOuqQx$C{i=_v4!#;+oc+K z(USv%RseN9R3|aw^xe;nI)OIgoSjo-ikgrMg*hXKEz>O^z#r#xm?Vo3aKZ%4sCN0AOt?PKsEat43faepNBf0V#}oIp`$;QIijrsbn}-Od72foMN(aDT;I`} z(ctn9!ci72LY({I3=5hnre`W-zg>@ZT`YvjsN3gRpwuv^%cp03Me8K!tSV7ehl?yw z>U=0P-J5Qsu?)ht3)b-B!rWME|mzPsfV4)3B3 zpHkkr32yG0_~pQ+j>;id81d1*wp*Z|TWAn%|64gXPz=9pYJZhA6Rp{{vutW{PU1Zp zx2O|(o9k@Zv=jQQ4pXMC11NQN?Ox6nO5sFKI;}+Buz*#Rb^#=LqK+- z__+GpY>`wSmu4{k`dpw<tQD1JP5G)Og+QK3ghny#8+XxEuI^`P&25 zP`Z_`pHD5t(83~hlCb3wTPXb`;kaDn_jM3bUX>v4sJK36MCCi3XAsK&4kWcjWgg1@ z`T<<EM|>n%dNLLpElDPkd9s~Hu7?*N-&TS@+T5H)pJn6(^OL8S}%JWkb1Muljz zeH))n`&N||J<(azdeBc?xAK1@0Bn0lB+aubGtu*Ioj@^hc5&w|3pD>5<KjIP@MiS&~C6l^e+Z;7`_(8du*BZ!E0)9 zL)xXWqD@&b4$V)#r8yK!P2Es-T6B5!PfZqOO1VRxs9qe)5)88;h6&eri ziMWWS1x<{BD!y8zW4@cCF}6Qg@(GC4BM5YahwK?JTYWPfZnmzsOWf#HBS4BzX* z2=}ME(h(Qf)~rUimkxqR;>2=142%o}q8fw`;~I|}%}s#rCR77B;@RHsa0`b4s@0a{ zcM+-GVZDRu9PlRg7;T7+Olg3?xVWY7L%J%b;0T`(9IPXG3Wu?bz^$#DE_!<^Gdp;656GVj{++mk zJX(=0j^^IO$?Z{}OTZ5kJN~o2O%-&MgU8aZ5&6e(i8pcADElPjvUxA9sAe+3{u!OnQXQ zfo7sV34>$q%ryho%EcdhTPy5iiq)L15U$FT&jw$G)qDm>z9AU7F#0=?uVr?2-2+0E zU<@Y4JaMpc$bkGOl5A%|-K%56A+#nr4x$d?;JeL1Fi@JIX}OTu50sk=ZCAqGsXsq1 zM{5BiFkidk>20@d8NRmgMl0OEp|Xibj}AF8Dga+a(^vA!@IiS7P{G*{2oR5+8^FNH zppvOn8iPlS+%*y6IH6?th=+i_crqfRfs!tP4%Lq|imQv(0s0zBIyO_7v7d^a#R=B3S{wu(*$dlEY#FQwg6w0hhCy z8Llp(Vu zzA_#D@_t;-JUGh1Krn@jSG?-1%cUN`90H2x@u2XR%SqV4h|4#swj?N_4tW(%WIwDpL;S#;?1=Ol@TT?748uV|kl7&2b*?8nuIkXN(-#l*#vjj61;Lf7^>iIV4B^V1m>fp79bNbo~v1O5K{XE{S8 zmnTb}gS_7^&;sD~9~ikyx*M1u-995`4?DG&5x%YEVxDo8h&J#p6kGvXHfHE-FnK_;1PXazoF&=d z)L{V^0dIr&7oH{YP=F%%h$|L7k1{Gii?@U9TXc#YEE&h?CCF$5;XX922G^ z-+noB778s#!6FcG(kXmgFl4-mmA@zU8w2NbP!sIkhKPaq+mVcjNNYfkE}? z!50|gs0qjDto-23*-F%@E`p_r9zC*DfOx!ii7ny3P~*A+A8`feI+TaO3<9<0KiVPZ z8J+6+$DPe<0s0zL6IXDq!+0(LFkY{Z=%Zh#_nv{z5I)|I%NcfsEtXDdu#v1w8i4M> zH-iU+zg&)V8C&Mx^elAr`$tB;x&t&6Fjx@iL$3;sj<@!KbGcD)?OF)VzmuA_(>1p2 ze|oRw8uL=ojn)F#ATTEgcfT$$I9~VJEf4xg0UEp&=?vGna<*tZu{2Hd)>aT%4j-|W zci&`7vOjiRna9Sfc8Yk>0ja)9A)mi305as-qthH+ zTKoiV1qz|rA8}|bg-e>OHMeB>!);%9e>gTDU(YoV6|&>P#2FV@Sscy?ONGE!eoA%@UHqpcZvfD!eq4AoR95eRh z0vH9mw(#+OT#no$0fGPKSfEock?Esm3QfG4GU?HQVl?owvnv8Q<073A0u8zZ!NGl@=U|9g@R#@FawIARTLQcVzD^l@4>*>Yu%4!mrYP?r~AVPh~hF)nlg`?G0rYP{SYIKR3f!XPadu;1XD#;aiX z3mFTm*kbXu+AYB-G7bo5;3Gz&#V^0W5!ApUF z@iy)&yj2CS_tz`7*p^$Mbgg*RXKgJsYJ7r^*ouGE2n3ARoB39L^)o=Cv`fTR-1iMz zES|Vx;gCT5$ZYqPEwL>#1)Yt(e>G&`M`rtX0z%_8%L&h%i`EO!<_mEdF6cd5Fd;_; zo$}EB%f0IXjxMgxgulEWmlIhl&;sD~@00GKmjp~|G*(^58sC~IgHHM5JMq`@0R0s{ zVl-C$K|pA{=GOz4=WGY)Blw6hK=Vho;2^#XuXMssmb<5;MIv-?mAHl={#lTGy!!6) z{cj-ks074y-#PW{=#(t8bOu;^G~*EJaByap)aiV_2)GD%8))6%d=iqIVdBtZUjktV^L`2hjMux)Y}~!q(5CI7O%o%Q zCBN8W@x;*V)eZO+l;UQ#MBZu@U2Cox(ql#;sBM6cxKeiRx1iW~)p{An@8ILK=RzG> zO{*p;wI%bS6a6aDZ&C>e9u8GZ9J8xB2ndbWd~daKEPi!R9CEa5^xV(5M;dKX^BC8cn_@!4`H$6(rHz&epioL7Ig6x&P#J!mYWfKUPIQ_Tr5`n+zC_jB zO-ev$d_508VG)nlEZ372_-{gWI?Y3ivPFJ@W;oM8YTKiF2?&kXtW)$X1HXP9+nX(z zPHgS|>fy-F)KMw24{IjMAx{T>tyH_|B!I=h%EXmFPG5n5@is0Cc9TfL>fJ5P7TfM9 z{5JG?Gkz4_*-s!~yxxhaX1DP5`wnuouy9(?kJ=rD#oeu=(9thy{l10ENCE)k^{Pi^ zoJF&Sos>~n91dW!!{UkhJ%21gEs8oKEga02*p|MZt`9ZW97Abgh0qau;wZcjlAp2@ z`{(5V+=%3%So##jcIEI{z{mS>IUC^u6g%+0=7xjEA6Ru6fO;UBI5&)!6%-p^)01Z% zxrkrSK0lN#`F~o^ei<>{_XtSu1k#D?*|*@4N_J)^6acS3$nEtveC*XQoHhO*>)Gm^ z^^iA_ptSUFBLsxTYo61qU&2lRBiC&UAMeNI43uLHCcof7*Uy((nRI;yWHhyy%_{-? z<#NWugM%#Vom>dK9pn>x*KPvj8Tg31u&YM1E5dl%5MlX_SJwEkD+HdrVI#Zk`3gFF zcYf_K5IET`4BFRvNJQDSb1)C1Ag3+gy)6W5&t!AIos-gz5r@Rfm-6Y)oYxf*m%`r7jX*l zep??dX0lWLk2&+m=Zd57b7s8>0z%_8R|mh*$FE+`QDF<_r6s0wAMR5}{JIG>txc(E z$BBYs<5d@H?K^`!=2iHJZ7fBV9hvub3Z3SY89$@zfC`&0q?OzhHNlYaiuDhcb;7UY zeo$x2rPJ!?YT1mu2-V6jCb4CYFbp;K$)ceYW6#>@BVy^VzBt^lCx{6E!zLHUf5<|GZIz)2yUq z{~14LmNQ@rCQMoBT;{?Ala2T}^F%{|769MQSIj#$ClL%!T^+JFVvTQq&OB&%&p3SS zWed6A*wuNv`xS$Q#as3(sN>@z6Lw(UTTOJ8J@9wUXM7a0Xu4p?_*(wBG9{`u|N%xgatyux_`i{%z-9z<9)mJ#11&;pOz^i_Zw+;Xh zU4qerkN4wpGHqA^@%p&kGVhCnDdG4CDXUSgEhF5YyrsA|!RXvUW})qi9ia39d^SAA z@R!R`ft<+<{7}24z+2+LX}3%Gd9M*XSHqwTt+%uJUMHhduPqkffUaj#=e9^I9G0-rT}ydRg7Hjg!z@HQHqa{C!ihF1fYddfE2L9ivjTVQj+j2ZYDM6)AX ze%lLew9?&^bbf+#>Is47`2s@YH7mZo;){BNpp-n$eZXV^NkWwtx__q4dX zsS2!2z2P?7MId0jjZ>m!Me%e03|F?;cIW;*jWk73fb$7H;@m$So&;j F|3z0YpG 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

L)P+xh$Sh<9d96%o#|0IYm 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�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

(stringifiedData: string): P { + return JSON.parse(this.crypto.base64Decode(stringifiedData)) + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Utils/StringToAuthenticatedData.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Utils/StringToAuthenticatedData.ts new file mode 100644 index 000000000..eb961e383 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Utils/StringToAuthenticatedData.ts @@ -0,0 +1,19 @@ +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { sortedCopy } from '@standardnotes/utils' +import { RootKeyEncryptedAuthenticatedData } from './../../../../Types/RootKeyEncryptedAuthenticatedData' +import { ItemAuthenticatedData } from './../../../../Types/ItemAuthenticatedData' + +export class StringToAuthenticatedDataUseCase { + constructor(private readonly crypto: PureCryptoInterface) {} + + execute( + rawAuthenticatedData: string, + override: ItemAuthenticatedData, + ): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData { + const base = JSON.parse(this.crypto.base64Decode(rawAuthenticatedData)) + return sortedCopy({ + ...base, + ...override, + }) + } +} diff --git a/packages/encryption/src/Domain/Operator/004/V004AlgorithmHelpers.ts b/packages/encryption/src/Domain/Operator/004/V004AlgorithmHelpers.ts new file mode 100644 index 000000000..93593f85a --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/V004AlgorithmHelpers.ts @@ -0,0 +1,20 @@ +import { V004Components, V004PartitionCharacter, V004StringComponents } from './V004AlgorithmTypes' + +export function doesPayloadRequireSigning(payload: { shared_vault_uuid?: string }) { + return payload.shared_vault_uuid != undefined +} + +export function deconstructEncryptedPayloadString(payloadString: string): V004Components { + /** Base64 encoding of JSON.stringify({}) */ + const EmptyAdditionalDataString = 'e30=' + + const components = payloadString.split(V004PartitionCharacter) as V004StringComponents + + return { + version: components[0], + nonce: components[1], + ciphertext: components[2], + authenticatedData: components[3], + additionalData: components[4] ?? EmptyAdditionalDataString, + } +} diff --git a/packages/encryption/src/Domain/Operator/004/V004AlgorithmTypes.ts b/packages/encryption/src/Domain/Operator/004/V004AlgorithmTypes.ts new file mode 100644 index 000000000..e16f92cc4 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/V004AlgorithmTypes.ts @@ -0,0 +1,32 @@ +export const V004AsymmetricCiphertextPrefix = '004_Asym' +export const V004PartitionCharacter = ':' + +export type V004StringComponents = [ + version: string, + nonce: string, + ciphertext: string, + authenticatedData: string, + additionalData: string, +] + +export type V004Components = { + version: V004StringComponents[0] + nonce: V004StringComponents[1] + ciphertext: V004StringComponents[2] + authenticatedData: V004StringComponents[3] + additionalData: V004StringComponents[4] +} + +export type V004AsymmetricStringComponents = [ + version: typeof V004AsymmetricCiphertextPrefix, + nonce: string, + ciphertext: string, + additionalData: string, +] + +export type V004AsymmetricComponents = { + version: V004AsymmetricStringComponents[0] + nonce: V004AsymmetricStringComponents[1] + ciphertext: V004AsymmetricStringComponents[2] + additionalData: V004AsymmetricStringComponents[3] +} diff --git a/packages/encryption/src/Domain/Operator/005/Operator005.spec.ts b/packages/encryption/src/Domain/Operator/005/Operator005.spec.ts deleted file mode 100644 index 50ce6cb2b..000000000 --- a/packages/encryption/src/Domain/Operator/005/Operator005.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { ProtocolOperator005 } from './Operator005' -import { PureCryptoInterface } from '@standardnotes/sncrypto-common' - -describe('operator 005', () => { - let crypto: PureCryptoInterface - let operator: ProtocolOperator005 - - beforeEach(() => { - crypto = {} as jest.Mocked - crypto.generateRandomKey = jest.fn().mockImplementation(() => { - return 'random-string' - }) - crypto.xchacha20Encrypt = jest.fn().mockImplementation((text: string) => { - return `${text}` - }) - crypto.xchacha20Decrypt = jest.fn().mockImplementation((text: string) => { - return text.split('')[1] - }) - crypto.sodiumCryptoBoxGenerateKeypair = jest.fn().mockImplementation(() => { - return { privateKey: 'private-key', publicKey: 'public-key', keyType: 'x25519' } - }) - crypto.sodiumCryptoBoxEasyEncrypt = jest.fn().mockImplementation((text: string) => { - return `${text}` - }) - crypto.sodiumCryptoBoxEasyDecrypt = jest.fn().mockImplementation((text: string) => { - return text.split('')[1] - }) - - operator = new ProtocolOperator005(crypto) - }) - - it('should generateKeyPair', () => { - const result = operator.generateKeyPair() - - expect(result).toEqual({ privateKey: 'private-key', publicKey: 'public-key', keyType: 'x25519' }) - }) - - it('should asymmetricEncryptKey', () => { - const senderKeypair = operator.generateKeyPair() - const recipientKeypair = operator.generateKeyPair() - - const plaintext = 'foo' - - const result = operator.asymmetricEncryptKey(plaintext, senderKeypair.privateKey, recipientKeypair.publicKey) - - expect(result).toEqual(`${'005_KeyAsym'}:random-string:foo`) - }) - - it('should asymmetricDecryptKey', () => { - const senderKeypair = operator.generateKeyPair() - const recipientKeypair = operator.generateKeyPair() - const plaintext = 'foo' - const ciphertext = operator.asymmetricEncryptKey(plaintext, senderKeypair.privateKey, recipientKeypair.publicKey) - const decrypted = operator.asymmetricDecryptKey(ciphertext, senderKeypair.publicKey, recipientKeypair.privateKey) - - expect(decrypted).toEqual('foo') - }) - - it('should symmetricEncryptPrivateKey', () => { - const keypair = operator.generateKeyPair() - const symmetricKey = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' - const encryptedKey = operator.symmetricEncryptPrivateKey(keypair.privateKey, symmetricKey) - - expect(encryptedKey).toEqual(`${'005_KeySym'}:random-string:${keypair.privateKey}`) - }) - - it('should symmetricDecryptPrivateKey', () => { - const keypair = operator.generateKeyPair() - const symmetricKey = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' - const encryptedKey = operator.symmetricEncryptPrivateKey(keypair.privateKey, symmetricKey) - const decryptedKey = operator.symmetricDecryptPrivateKey(encryptedKey, symmetricKey) - - expect(decryptedKey).toEqual(keypair.privateKey) - }) -}) diff --git a/packages/encryption/src/Domain/Operator/005/Operator005.ts b/packages/encryption/src/Domain/Operator/005/Operator005.ts deleted file mode 100644 index d2d743a21..000000000 --- a/packages/encryption/src/Domain/Operator/005/Operator005.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { ProtocolVersion } from '@standardnotes/common' -import { Base64String, HexString, PkcKeyPair, Utf8String } from '@standardnotes/sncrypto-common' -import { V005Algorithm } from '../../Algorithm' -import { SNProtocolOperator004 } from '../004/Operator004' - -const VersionString = '005' -const SymmetricCiphertextPrefix = `${VersionString}_KeySym` -const AsymmetricCiphertextPrefix = `${VersionString}_KeyAsym` - -export type AsymmetricallyEncryptedKey = Base64String -export type SymmetricallyEncryptedPrivateKey = Base64String - -/** - * @experimental - * @unreleased - */ -export class ProtocolOperator005 extends SNProtocolOperator004 { - public override getEncryptionDisplayName(): string { - return 'XChaCha20-Poly1305' - } - - override get version(): ProtocolVersion { - return VersionString as ProtocolVersion - } - - generateKeyPair(): PkcKeyPair { - return this.crypto.sodiumCryptoBoxGenerateKeypair() - } - - asymmetricEncryptKey( - keyToEncrypt: HexString, - senderSecretKey: HexString, - recipientPublicKey: HexString, - ): AsymmetricallyEncryptedKey { - const nonce = this.crypto.generateRandomKey(V005Algorithm.AsymmetricEncryptionNonceLength) - - const ciphertext = this.crypto.sodiumCryptoBoxEasyEncrypt(keyToEncrypt, nonce, senderSecretKey, recipientPublicKey) - - return [AsymmetricCiphertextPrefix, nonce, ciphertext].join(':') - } - - asymmetricDecryptKey( - keyToDecrypt: AsymmetricallyEncryptedKey, - senderPublicKey: HexString, - recipientSecretKey: HexString, - ): Utf8String { - const components = keyToDecrypt.split(':') - - const nonce = components[1] - - return this.crypto.sodiumCryptoBoxEasyDecrypt(keyToDecrypt, nonce, senderPublicKey, recipientSecretKey) - } - - symmetricEncryptPrivateKey(privateKey: HexString, symmetricKey: HexString): SymmetricallyEncryptedPrivateKey { - if (symmetricKey.length !== 64) { - throw new Error('Symmetric key length must be 256 bits') - } - - const nonce = this.crypto.generateRandomKey(V005Algorithm.SymmetricEncryptionNonceLength) - - const encryptedKey = this.crypto.xchacha20Encrypt(privateKey, nonce, symmetricKey) - - return [SymmetricCiphertextPrefix, nonce, encryptedKey].join(':') - } - - symmetricDecryptPrivateKey( - encryptedPrivateKey: SymmetricallyEncryptedPrivateKey, - symmetricKey: HexString, - ): HexString | null { - if (symmetricKey.length !== 64) { - throw new Error('Symmetric key length must be 256 bits') - } - - const components = encryptedPrivateKey.split(':') - - const nonce = components[1] - - return this.crypto.xchacha20Decrypt(encryptedPrivateKey, nonce, symmetricKey) - } -} diff --git a/packages/encryption/src/Domain/Operator/Functions.ts b/packages/encryption/src/Domain/Operator/Functions.ts index e5eb25f99..53125e2c7 100644 --- a/packages/encryption/src/Domain/Operator/Functions.ts +++ b/packages/encryption/src/Domain/Operator/Functions.ts @@ -4,12 +4,9 @@ import { SNProtocolOperator001 } from '../Operator/001/Operator001' import { SNProtocolOperator002 } from '../Operator/002/Operator002' import { SNProtocolOperator003 } from '../Operator/003/Operator003' import { SNProtocolOperator004 } from '../Operator/004/Operator004' -import { AsynchronousOperator, SynchronousOperator } from '../Operator/Operator' +import { AnyOperatorInterface } from './OperatorInterface/TypeCheck' -export function createOperatorForVersion( - version: ProtocolVersion, - crypto: PureCryptoInterface, -): AsynchronousOperator | SynchronousOperator { +export function createOperatorForVersion(version: ProtocolVersion, crypto: PureCryptoInterface): AnyOperatorInterface { if (version === ProtocolVersion.V001) { return new SNProtocolOperator001(crypto) } else if (version === ProtocolVersion.V002) { @@ -22,9 +19,3 @@ export function createOperatorForVersion( throw Error(`Unable to find operator for version ${version}`) } } - -export function isAsyncOperator( - operator: AsynchronousOperator | SynchronousOperator, -): operator is AsynchronousOperator { - return (operator as AsynchronousOperator).generateDecryptedParametersAsync !== undefined -} diff --git a/packages/encryption/src/Domain/Operator/Operator.ts b/packages/encryption/src/Domain/Operator/Operator.ts deleted file mode 100644 index dd1b5d533..000000000 --- a/packages/encryption/src/Domain/Operator/Operator.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { KeyParamsOrigination } from '@standardnotes/common' -import * as Models from '@standardnotes/models' -import { ItemsKeyInterface, RootKeyInterface } from '@standardnotes/models' -import { SNRootKey } from '../Keys/RootKey/RootKey' -import { SNRootKeyParams } from '../Keys/RootKey/RootKeyParams' -import { DecryptedParameters, EncryptedParameters, ErrorDecryptingParameters } from '../Types/EncryptedParameters' -import { ItemAuthenticatedData } from '../Types/ItemAuthenticatedData' -import { LegacyAttachedData } from '../Types/LegacyAttachedData' -import { RootKeyEncryptedAuthenticatedData } from '../Types/RootKeyEncryptedAuthenticatedData' - -/**w - * An operator is responsible for performing crypto operations, such as generating keys - * and encrypting/decrypting payloads. Operators interact directly with - * platform dependent SNPureCrypto implementation to directly access cryptographic primitives. - * Each operator is versioned according to the protocol version. Functions that are common - * across all versions appear in this generic parent class. - */ -export interface OperatorCommon { - createItemsKey(): ItemsKeyInterface - /** - * Returns encryption protocol display name - */ - getEncryptionDisplayName(): string - - readonly version: string - - /** - * Returns the payload's authenticated data. The passed payload must be in a - * non-decrypted, ciphertext state. - */ - getPayloadAuthenticatedData( - encrypted: EncryptedParameters, - ): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined - - /** - * Computes a root key given a password and previous keyParams - * @param password - Plain string representing raw user password - */ - computeRootKey(password: string, keyParams: SNRootKeyParams): Promise - - /** - * Creates a new root key given an identifier and a user password - * @param identifier - Plain string representing a unique identifier - * for the user - * @param password - Plain string representing raw user password - */ - createRootKey(identifier: string, password: string, origination: KeyParamsOrigination): Promise -} - -export interface SynchronousOperator extends OperatorCommon { - /** - * Converts a bare payload into an encrypted one in the desired format. - * @param payload - The non-encrypted payload object to encrypt - * @param key - The key to use to encrypt the payload. Can be either - * a RootKey (when encrypting payloads that require root key encryption, such as encrypting - * items keys), or an ItemsKey (if encrypted regular items) - */ - generateEncryptedParametersSync( - payload: Models.DecryptedPayloadInterface, - key: ItemsKeyInterface | RootKeyInterface, - ): EncryptedParameters - - generateDecryptedParametersSync( - encrypted: EncryptedParameters, - key: ItemsKeyInterface | RootKeyInterface, - ): DecryptedParameters | ErrorDecryptingParameters -} - -export interface AsynchronousOperator extends OperatorCommon { - /** - * Converts a bare payload into an encrypted one in the desired format. - * @param payload - The non-encrypted payload object to encrypt - * @param key - The key to use to encrypt the payload. Can be either - * a RootKey (when encrypting payloads that require root key encryption, such as encrypting - * items keys), or an ItemsKey (if encrypted regular items) - */ - generateEncryptedParametersAsync( - payload: Models.DecryptedPayloadInterface, - key: ItemsKeyInterface | RootKeyInterface, - ): Promise - - generateDecryptedParametersAsync( - encrypted: EncryptedParameters, - key: ItemsKeyInterface | RootKeyInterface, - ): Promise | ErrorDecryptingParameters> -} diff --git a/packages/encryption/src/Domain/Operator/OperatorInterface/AsyncOperatorInterface.ts b/packages/encryption/src/Domain/Operator/OperatorInterface/AsyncOperatorInterface.ts new file mode 100644 index 000000000..c029ca7e1 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/OperatorInterface/AsyncOperatorInterface.ts @@ -0,0 +1,22 @@ +import { + DecryptedPayloadInterface, + ItemContent, + ItemsKeyInterface, + KeySystemItemsKeyInterface, + KeySystemRootKeyInterface, + RootKeyInterface, +} from '@standardnotes/models' +import { EncryptedOutputParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters' +import { DecryptedParameters } from '../../Types/DecryptedParameters' + +export interface AsyncOperatorInterface { + generateEncryptedParametersAsync( + payload: DecryptedPayloadInterface, + key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface, + ): Promise + + generateDecryptedParametersAsync( + encrypted: EncryptedOutputParameters, + key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface, + ): Promise | ErrorDecryptingParameters> +} diff --git a/packages/encryption/src/Domain/Operator/OperatorInterface/OperatorInterface.ts b/packages/encryption/src/Domain/Operator/OperatorInterface/OperatorInterface.ts new file mode 100644 index 000000000..1b51975e8 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/OperatorInterface/OperatorInterface.ts @@ -0,0 +1,102 @@ +import { KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common' +import { + ItemsKeyInterface, + RootKeyInterface, + KeySystemItemsKeyInterface, + KeySystemRootKeyInterface, + KeySystemIdentifier, + KeySystemRootKeyParamsInterface, +} from '@standardnotes/models' +import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams' +import { EncryptedOutputParameters } from '../../Types/EncryptedParameters' +import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData' +import { LegacyAttachedData } from '../../Types/LegacyAttachedData' +import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData' +import { HexString, PkcKeyPair } from '@standardnotes/sncrypto-common' +import { AsymmetricallyEncryptedString } from '../Types/Types' +import { AsymmetricDecryptResult } from '../Types/AsymmetricDecryptResult' +import { PublicKeySet } from '../Types/PublicKeySet' +import { AsymmetricSignatureVerificationDetachedResult } from '../Types/AsymmetricSignatureVerificationDetachedResult' + +/**w + * An operator is responsible for performing crypto operations, such as generating keys + * and encrypting/decrypting payloads. Operators interact directly with + * platform dependent SNPureCrypto implementation to directly access cryptographic primitives. + * Each operator is versioned according to the protocol version. Functions that are common + * across all versions appear in this generic parent class. + */ +export interface OperatorInterface { + /** + * Returns encryption protocol display name + */ + getEncryptionDisplayName(): string + + readonly version: string + + createItemsKey(): ItemsKeyInterface + + /** + * Returns the payload's authenticated data. The passed payload must be in a + * non-decrypted, ciphertext state. + */ + getPayloadAuthenticatedDataForExternalUse( + encrypted: EncryptedOutputParameters, + ): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined + + /** + * Computes a root key given a password and previous keyParams + * @param password - Plain string representing raw user password + */ + computeRootKey(password: string, keyParams: SNRootKeyParams): Promise + + /** + * Creates a new root key given an identifier and a user password + * @param identifier - Plain string representing a unique identifier + * for the user + * @param password - Plain string representing raw user password + */ + createRootKey( + identifier: string, + password: string, + origination: KeyParamsOrigination, + ): Promise + + createRandomizedKeySystemRootKey(dto: { systemIdentifier: KeySystemIdentifier }): KeySystemRootKeyInterface + + createUserInputtedKeySystemRootKey(dto: { + systemIdentifier: KeySystemIdentifier + userInputtedPassword: string + }): KeySystemRootKeyInterface + + deriveUserInputtedKeySystemRootKey(dto: { + keyParams: KeySystemRootKeyParamsInterface + userInputtedPassword: string + }): KeySystemRootKeyInterface + + createKeySystemItemsKey( + uuid: string, + keySystemIdentifier: KeySystemIdentifier, + sharedVaultUuid: string | undefined, + rootKeyToken: string, + ): KeySystemItemsKeyInterface + + asymmetricEncrypt(dto: { + stringToEncrypt: HexString + senderKeyPair: PkcKeyPair + senderSigningKeyPair: PkcKeyPair + recipientPublicKey: HexString + }): AsymmetricallyEncryptedString + + asymmetricDecrypt(dto: { + stringToDecrypt: AsymmetricallyEncryptedString + recipientSecretKey: HexString + }): AsymmetricDecryptResult | null + + asymmetricSignatureVerifyDetached( + encryptedString: AsymmetricallyEncryptedString, + ): AsymmetricSignatureVerificationDetachedResult + + getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: AsymmetricallyEncryptedString): PublicKeySet + + versionForAsymmetricallyEncryptedString(encryptedString: string): ProtocolVersion +} diff --git a/packages/encryption/src/Domain/Operator/OperatorInterface/SyncOperatorInterface.ts b/packages/encryption/src/Domain/Operator/OperatorInterface/SyncOperatorInterface.ts new file mode 100644 index 000000000..fa2c5d9d5 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/OperatorInterface/SyncOperatorInterface.ts @@ -0,0 +1,31 @@ +import { + DecryptedPayloadInterface, + ItemContent, + ItemsKeyInterface, + KeySystemItemsKeyInterface, + KeySystemRootKeyInterface, + RootKeyInterface, +} from '@standardnotes/models' +import { PkcKeyPair } from '@standardnotes/sncrypto-common' +import { EncryptedOutputParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters' +import { DecryptedParameters } from '../../Types/DecryptedParameters' + +export interface SyncOperatorInterface { + /** + * Converts a bare payload into an encrypted one in the desired format. + * @param payload - The non-encrypted payload object to encrypt + * @param key - The key to use to encrypt the payload. Can be either + * a RootKey (when encrypting payloads that require root key encryption, such as encrypting + * items keys), or an ItemsKey (if encrypted regular items) + */ + generateEncryptedParameters( + payload: DecryptedPayloadInterface, + key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface, + signingKeyPair?: PkcKeyPair, + ): EncryptedOutputParameters + + generateDecryptedParameters( + encrypted: EncryptedOutputParameters, + key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface, + ): DecryptedParameters | ErrorDecryptingParameters +} diff --git a/packages/encryption/src/Domain/Operator/OperatorInterface/TypeCheck.ts b/packages/encryption/src/Domain/Operator/OperatorInterface/TypeCheck.ts new file mode 100644 index 000000000..38b9b4c2a --- /dev/null +++ b/packages/encryption/src/Domain/Operator/OperatorInterface/TypeCheck.ts @@ -0,0 +1,13 @@ +import { AsyncOperatorInterface } from './AsyncOperatorInterface' +import { OperatorInterface } from './OperatorInterface' +import { SyncOperatorInterface } from './SyncOperatorInterface' + +export type AnyOperatorInterface = OperatorInterface & (AsyncOperatorInterface | SyncOperatorInterface) + +export function isAsyncOperator(operator: unknown): operator is AsyncOperatorInterface { + return 'generateEncryptedParametersAsync' in (operator as AsyncOperatorInterface) +} + +export function isSyncOperator(operator: unknown): operator is SyncOperatorInterface { + return !isAsyncOperator(operator) +} diff --git a/packages/encryption/src/Domain/Operator/OperatorManager.ts b/packages/encryption/src/Domain/Operator/OperatorManager.ts index 5441bcb31..82b710ad9 100644 --- a/packages/encryption/src/Domain/Operator/OperatorManager.ts +++ b/packages/encryption/src/Domain/Operator/OperatorManager.ts @@ -1,10 +1,10 @@ import { ProtocolVersion, ProtocolVersionLatest } from '@standardnotes/common' import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { createOperatorForVersion } from './Functions' -import { AsynchronousOperator, SynchronousOperator } from './Operator' +import { AnyOperatorInterface } from './OperatorInterface/TypeCheck' export class OperatorManager { - private operators: Record = {} + private operators: Record = {} constructor(private crypto: PureCryptoInterface) { this.crypto = crypto @@ -15,7 +15,7 @@ export class OperatorManager { this.operators = {} } - public operatorForVersion(version: ProtocolVersion): SynchronousOperator | AsynchronousOperator { + public operatorForVersion(version: ProtocolVersion): AnyOperatorInterface { const operatorKey = version let operator = this.operators[operatorKey] if (!operator) { @@ -28,7 +28,7 @@ export class OperatorManager { /** * Returns the operator corresponding to the latest protocol version */ - public defaultOperator(): SynchronousOperator | AsynchronousOperator { + public defaultOperator(): AnyOperatorInterface { return this.operatorForVersion(ProtocolVersionLatest) } } diff --git a/packages/encryption/src/Domain/Operator/OperatorWrapper.ts b/packages/encryption/src/Domain/Operator/OperatorWrapper.ts index 323d8dcb5..c2aed9c59 100644 --- a/packages/encryption/src/Domain/Operator/OperatorWrapper.ts +++ b/packages/encryption/src/Domain/Operator/OperatorWrapper.ts @@ -4,49 +4,53 @@ import { RootKeyInterface, ItemContent, EncryptedPayloadInterface, + KeySystemItemsKeyInterface, + KeySystemRootKeyInterface, } from '@standardnotes/models' import { - DecryptedParameters, - EncryptedParameters, - encryptedParametersFromPayload, + EncryptedOutputParameters, + encryptedInputParametersFromPayload, ErrorDecryptingParameters, } from '../Types/EncryptedParameters' -import { isAsyncOperator } from './Functions' +import { DecryptedParameters } from '../Types/DecryptedParameters' import { OperatorManager } from './OperatorManager' +import { PkcKeyPair } from '@standardnotes/sncrypto-common' +import { isAsyncOperator } from './OperatorInterface/TypeCheck' export async function encryptPayload( payload: DecryptedPayloadInterface, - key: ItemsKeyInterface | RootKeyInterface, + key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface, operatorManager: OperatorManager, -): Promise { + signingKeyPair: PkcKeyPair | undefined, +): Promise { const operator = operatorManager.operatorForVersion(key.keyVersion) - let encryptionParameters + let result: EncryptedOutputParameters | undefined = undefined if (isAsyncOperator(operator)) { - encryptionParameters = await operator.generateEncryptedParametersAsync(payload, key) + result = await operator.generateEncryptedParametersAsync(payload, key) } else { - encryptionParameters = operator.generateEncryptedParametersSync(payload, key) + result = operator.generateEncryptedParameters(payload, key, signingKeyPair) } - if (!encryptionParameters) { + if (!result) { throw 'Unable to generate encryption parameters' } - return encryptionParameters + return result } export async function decryptPayload( payload: EncryptedPayloadInterface, - key: ItemsKeyInterface | RootKeyInterface, + key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface, operatorManager: OperatorManager, ): Promise | ErrorDecryptingParameters> { const operator = operatorManager.operatorForVersion(payload.version) try { if (isAsyncOperator(operator)) { - return await operator.generateDecryptedParametersAsync(encryptedParametersFromPayload(payload), key) + return await operator.generateDecryptedParametersAsync(encryptedInputParametersFromPayload(payload), key) } else { - return operator.generateDecryptedParametersSync(encryptedParametersFromPayload(payload), key) + return operator.generateDecryptedParameters(encryptedInputParametersFromPayload(payload), key) } } catch (e) { console.error('Error decrypting payload', payload, e) diff --git a/packages/encryption/src/Domain/Operator/Types/AsymmetricDecryptResult.ts b/packages/encryption/src/Domain/Operator/Types/AsymmetricDecryptResult.ts new file mode 100644 index 000000000..28cc8f061 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/Types/AsymmetricDecryptResult.ts @@ -0,0 +1,8 @@ +import { HexString } from '@standardnotes/sncrypto-common' + +export type AsymmetricDecryptResult = { + plaintext: HexString + signatureVerified: boolean + signaturePublicKey: string + senderPublicKey: string +} diff --git a/packages/encryption/src/Domain/Operator/Types/AsymmetricSignatureVerificationDetachedResult.ts b/packages/encryption/src/Domain/Operator/Types/AsymmetricSignatureVerificationDetachedResult.ts new file mode 100644 index 000000000..5f3cdecb6 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/Types/AsymmetricSignatureVerificationDetachedResult.ts @@ -0,0 +1,9 @@ +export type AsymmetricSignatureVerificationDetachedResult = + | { + signatureVerified: true + signaturePublicKey: string + senderPublicKey: string + } + | { + signatureVerified: false + } diff --git a/packages/encryption/src/Domain/Operator/Types/PublicKeySet.ts b/packages/encryption/src/Domain/Operator/Types/PublicKeySet.ts new file mode 100644 index 000000000..20c35d9c3 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/Types/PublicKeySet.ts @@ -0,0 +1,4 @@ +export type PublicKeySet = { + encryption: string + signing: string +} diff --git a/packages/encryption/src/Domain/Operator/Types/Types.ts b/packages/encryption/src/Domain/Operator/Types/Types.ts new file mode 100644 index 000000000..f37bf6062 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/Types/Types.ts @@ -0,0 +1,4 @@ +import { Base64String } from '@standardnotes/sncrypto-common' + +export type AsymmetricallyEncryptedString = Base64String +export type SymmetricallyEncryptedString = Base64String diff --git a/packages/encryption/src/Domain/Service/Encryption/EncryptionProviderInterface.ts b/packages/encryption/src/Domain/Service/Encryption/EncryptionProviderInterface.ts index 133133c77..39d62fb4c 100644 --- a/packages/encryption/src/Domain/Service/Encryption/EncryptionProviderInterface.ts +++ b/packages/encryption/src/Domain/Service/Encryption/EncryptionProviderInterface.ts @@ -1,3 +1,4 @@ +import { AsymmetricSignatureVerificationDetachedResult } from '../../Operator/Types/AsymmetricSignatureVerificationDetachedResult' import { KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common' import { BackupFile, @@ -6,17 +7,26 @@ import { ItemContent, ItemsKeyInterface, RootKeyInterface, + KeySystemIdentifier, + KeySystemItemsKeyInterface, + AsymmetricMessagePayload, + KeySystemRootKeyInterface, + KeySystemRootKeyParamsInterface, + TrustedContactInterface, } from '@standardnotes/models' import { ClientDisplayableError } from '@standardnotes/responses' - import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams' import { KeyedDecryptionSplit } from '../../Split/KeyedDecryptionSplit' import { KeyedEncryptionSplit } from '../../Split/KeyedEncryptionSplit' import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData' -import { LegacyAttachedData } from '../../Types/LegacyAttachedData' -import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData' +import { PkcKeyPair } from '@standardnotes/sncrypto-common' +import { PublicKeySet } from '../../Operator/Types/PublicKeySet' +import { KeySystemKeyManagerInterface } from '../KeySystemKeyManagerInterface' +import { AsymmetricallyEncryptedString } from '../../Operator/Types/Types' export interface EncryptionProviderInterface { + keys: KeySystemKeyManagerInterface + encryptSplitSingle(split: KeyedEncryptionSplit): Promise encryptSplit(split: KeyedEncryptionSplit): Promise decryptSplitSingle< @@ -31,29 +41,24 @@ export interface EncryptionProviderInterface { >( split: KeyedDecryptionSplit, ): Promise<(P | EncryptedPayloadInterface)[]> - hasRootKeyEncryptionSource(): boolean - getKeyEmbeddedKeyParams(key: EncryptedPayloadInterface): SNRootKeyParams | undefined - computeRootKey(password: string, keyParams: SNRootKeyParams): Promise + + getEmbeddedPayloadAuthenticatedData( + payload: EncryptedPayloadInterface, + ): D | undefined + getKeyEmbeddedKeyParamsFromItemsKey(key: EncryptedPayloadInterface): SNRootKeyParams | undefined + supportedVersions(): ProtocolVersion[] isVersionNewerThanLibraryVersion(version: ProtocolVersion): boolean platformSupportsKeyDerivation(keyParams: SNRootKeyParams): boolean - computeWrappingKey(passcode: string): Promise - getUserVersion(): ProtocolVersion | undefined + decryptBackupFile( file: BackupFile, password?: string, ): Promise + + getUserVersion(): ProtocolVersion | undefined hasAccount(): boolean - decryptErroredPayloads(): Promise - deleteWorkspaceSpecificKeyStateFromDevice(): Promise hasPasscode(): boolean - createRootKey( - identifier: string, - password: string, - origination: KeyParamsOrigination, - version?: ProtocolVersion, - ): Promise - setNewRootKeyWrapper(wrappingKey: RootKeyInterface): Promise removePasscode(): Promise validateAccountPassword(password: string): Promise< | { @@ -66,11 +71,63 @@ export interface EncryptionProviderInterface { valid: boolean } > + + decryptErroredPayloads(): Promise + deleteWorkspaceSpecificKeyStateFromDevice(): Promise + + computeRootKey(password: string, keyParams: SNRootKeyParams): Promise + computeWrappingKey(passcode: string): Promise + hasRootKeyEncryptionSource(): boolean + createRootKey( + identifier: string, + password: string, + origination: KeyParamsOrigination, + version?: ProtocolVersion, + ): Promise + getRootKeyParams(): SNRootKeyParams | undefined + setNewRootKeyWrapper(wrappingKey: RootKeyInterface): Promise + createNewItemsKeyWithRollback(): Promise<() => Promise> - reencryptItemsKeys(): Promise + reencryptApplicableItemsAfterUserRootKeyChange(): Promise getSureDefaultItemsKey(): ItemsKeyInterface - getRootKeyParams(): Promise - getEmbeddedPayloadAuthenticatedData( - payload: EncryptedPayloadInterface, - ): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined + + createRandomizedKeySystemRootKey(dto: { systemIdentifier: KeySystemIdentifier }): KeySystemRootKeyInterface + + createUserInputtedKeySystemRootKey(dto: { + systemIdentifier: KeySystemIdentifier + userInputtedPassword: string + }): KeySystemRootKeyInterface + + deriveUserInputtedKeySystemRootKey(dto: { + keyParams: KeySystemRootKeyParamsInterface + userInputtedPassword: string + }): KeySystemRootKeyInterface + + createKeySystemItemsKey( + uuid: string, + keySystemIdentifier: KeySystemIdentifier, + sharedVaultUuid: string | undefined, + rootKeyToken: string, + ): KeySystemItemsKeyInterface + + reencryptKeySystemItemsKeysForVault(keySystemIdentifier: KeySystemIdentifier): Promise + + getKeyPair(): PkcKeyPair + getSigningKeyPair(): PkcKeyPair + + asymmetricallyEncryptMessage(dto: { + message: AsymmetricMessagePayload + senderKeyPair: PkcKeyPair + senderSigningKeyPair: PkcKeyPair + recipientPublicKey: string + }): string + asymmetricallyDecryptMessage(dto: { + encryptedString: AsymmetricallyEncryptedString + trustedSender: TrustedContactInterface | undefined + privateKey: string + }): M | undefined + asymmetricSignatureVerifyDetached( + encryptedString: AsymmetricallyEncryptedString, + ): AsymmetricSignatureVerificationDetachedResult + getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: string): PublicKeySet } diff --git a/packages/encryption/src/Domain/Service/KeySystemKeyManagerInterface.ts b/packages/encryption/src/Domain/Service/KeySystemKeyManagerInterface.ts new file mode 100644 index 000000000..38afcaade --- /dev/null +++ b/packages/encryption/src/Domain/Service/KeySystemKeyManagerInterface.ts @@ -0,0 +1,31 @@ +import { + EncryptedItemInterface, + KeySystemIdentifier, + KeySystemItemsKeyInterface, + KeySystemRootKeyInterface, + KeySystemRootKeyStorageMode, + VaultListingInterface, +} from '@standardnotes/models' + +export interface KeySystemKeyManagerInterface { + getAllKeySystemItemsKeys(): (KeySystemItemsKeyInterface | EncryptedItemInterface)[] + getKeySystemItemsKeys(systemIdentifier: KeySystemIdentifier): KeySystemItemsKeyInterface[] + getPrimaryKeySystemItemsKey(systemIdentifier: KeySystemIdentifier): KeySystemItemsKeyInterface + + /** Returns synced root keys, in addition to any local or ephemeral keys */ + getAllKeySystemRootKeysForVault(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface[] + getSyncedKeySystemRootKeysForVault(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface[] + getAllSyncedKeySystemRootKeys(): KeySystemRootKeyInterface[] + getKeySystemRootKeyWithToken( + systemIdentifier: KeySystemIdentifier, + keyIdentifier: string, + ): KeySystemRootKeyInterface | undefined + getPrimaryKeySystemRootKey(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface | undefined + + intakeNonPersistentKeySystemRootKey(key: KeySystemRootKeyInterface, storage: KeySystemRootKeyStorageMode): void + undoIntakeNonPersistentKeySystemRootKey(systemIdentifier: KeySystemIdentifier): void + + clearMemoryOfKeysRelatedToVault(vault: VaultListingInterface): void + deleteNonPersistentSystemRootKeysForVault(systemIdentifier: KeySystemIdentifier): Promise + deleteAllSyncedKeySystemRootKeys(systemIdentifier: KeySystemIdentifier): Promise +} diff --git a/packages/encryption/src/Domain/Split/AbstractKeySplit.ts b/packages/encryption/src/Domain/Split/AbstractKeySplit.ts index 59a46b008..acc3ec37f 100644 --- a/packages/encryption/src/Domain/Split/AbstractKeySplit.ts +++ b/packages/encryption/src/Domain/Split/AbstractKeySplit.ts @@ -1,8 +1,10 @@ import { DecryptedPayloadInterface, EncryptedPayloadInterface, + KeySystemRootKeyInterface, ItemsKeyInterface, RootKeyInterface, + KeySystemItemsKeyInterface, } from '@standardnotes/models' export interface AbstractKeySplit { @@ -10,13 +12,20 @@ export interface AbstractKeySplit item.uuid === uuid) + if (inUsesKeySystemRootKey) { + return inUsesKeySystemRootKey + } + const inUsesItemsKeyWithKeyLookup = split.usesItemsKeyWithKeyLookup?.items.find((item) => item.uuid === uuid) if (inUsesItemsKeyWithKeyLookup) { return inUsesItemsKeyWithKeyLookup @@ -56,6 +69,13 @@ export function FindPayloadInEncryptionSplit(uuid: string, split: KeyedEncryptio return inUsesRootKeyWithKeyLookup } + const inUsesKeySystemRootKeyWithKeyLookup = split.usesKeySystemRootKeyWithKeyLookup?.items.find( + (item) => item.uuid === uuid, + ) + if (inUsesKeySystemRootKeyWithKeyLookup) { + return inUsesKeySystemRootKeyWithKeyLookup + } + throw Error('Cannot find payload in encryption split') } @@ -70,6 +90,11 @@ export function FindPayloadInDecryptionSplit(uuid: string, split: KeyedDecryptio return inUsesRootKey } + const inUsesKeySystemRootKey = split.usesKeySystemRootKey?.items.find((item) => item.uuid === uuid) + if (inUsesKeySystemRootKey) { + return inUsesKeySystemRootKey + } + const inUsesItemsKeyWithKeyLookup = split.usesItemsKeyWithKeyLookup?.items.find((item) => item.uuid === uuid) if (inUsesItemsKeyWithKeyLookup) { return inUsesItemsKeyWithKeyLookup @@ -80,5 +105,12 @@ export function FindPayloadInDecryptionSplit(uuid: string, split: KeyedDecryptio return inUsesRootKeyWithKeyLookup } + const inUsesKeySystemRootKeyWithKeyLookup = split.usesKeySystemRootKeyWithKeyLookup?.items.find( + (item) => item.uuid === uuid, + ) + if (inUsesKeySystemRootKeyWithKeyLookup) { + return inUsesKeySystemRootKeyWithKeyLookup + } + throw Error('Cannot find payload in encryption split') } diff --git a/packages/encryption/src/Domain/Split/EncryptionTypeSplit.ts b/packages/encryption/src/Domain/Split/EncryptionTypeSplit.ts index 34f5def1f..d032f6a2d 100644 --- a/packages/encryption/src/Domain/Split/EncryptionTypeSplit.ts +++ b/packages/encryption/src/Domain/Split/EncryptionTypeSplit.ts @@ -2,5 +2,6 @@ import { DecryptedPayloadInterface, EncryptedPayloadInterface } from '@standardn export interface EncryptionTypeSplit { rootKeyEncryption?: T[] + keySystemRootKeyEncryption?: T[] itemsKeyEncryption?: T[] } diff --git a/packages/encryption/src/Domain/Split/Functions.ts b/packages/encryption/src/Domain/Split/Functions.ts index ea39bcebf..e36065590 100644 --- a/packages/encryption/src/Domain/Split/Functions.ts +++ b/packages/encryption/src/Domain/Split/Functions.ts @@ -1,5 +1,9 @@ -import { DecryptedPayloadInterface, EncryptedPayloadInterface } from '@standardnotes/models' -import { ItemContentTypeUsesRootKeyEncryption } from '../Keys/RootKey/Functions' +import { + DecryptedPayloadInterface, + EncryptedPayloadInterface, + ContentTypeUsesKeySystemRootKeyEncryption, + ContentTypeUsesRootKeyEncryption, +} from '@standardnotes/models' import { EncryptionTypeSplit } from './EncryptionTypeSplit' export function SplitPayloadsByEncryptionType( @@ -7,10 +11,13 @@ export function SplitPayloadsByEncryptionType { const usesRootKey: T[] = [] const usesItemsKey: T[] = [] + const usesKeySystemRootKey: T[] = [] for (const item of payloads) { - if (ItemContentTypeUsesRootKeyEncryption(item.content_type)) { + if (ContentTypeUsesRootKeyEncryption(item.content_type)) { usesRootKey.push(item) + } else if (ContentTypeUsesKeySystemRootKeyEncryption(item.content_type)) { + usesKeySystemRootKey.push(item) } else { usesItemsKey.push(item) } @@ -19,5 +26,6 @@ export function SplitPayloadsByEncryptionType 0 ? usesRootKey : undefined, itemsKeyEncryption: usesItemsKey.length > 0 ? usesItemsKey : undefined, + keySystemRootKeyEncryption: usesKeySystemRootKey.length > 0 ? usesKeySystemRootKey : undefined, } } diff --git a/packages/encryption/src/Domain/Types/DecryptedParameters.ts b/packages/encryption/src/Domain/Types/DecryptedParameters.ts new file mode 100644 index 000000000..de54a3558 --- /dev/null +++ b/packages/encryption/src/Domain/Types/DecryptedParameters.ts @@ -0,0 +1,7 @@ +import { ItemContent, PersistentSignatureData } from '@standardnotes/models' + +export type DecryptedParameters = { + uuid: string + content: C + signatureData: PersistentSignatureData +} diff --git a/packages/encryption/src/Domain/Types/EncryptedParameters.ts b/packages/encryption/src/Domain/Types/EncryptedParameters.ts index b5ef78996..1640f0fc2 100644 --- a/packages/encryption/src/Domain/Types/EncryptedParameters.ts +++ b/packages/encryption/src/Domain/Types/EncryptedParameters.ts @@ -1,20 +1,23 @@ -import { ProtocolVersion } from '@standardnotes/common' -import { EncryptedPayloadInterface, ItemContent } from '@standardnotes/models' +import { ContentType, ProtocolVersion } from '@standardnotes/common' +import { EncryptedPayloadInterface, DecryptedPayloadInterface, PersistentSignatureData } from '@standardnotes/models' +import { DecryptedParameters } from './DecryptedParameters' -export type EncryptedParameters = { +export type EncryptedOutputParameters = { uuid: string content: string + content_type: ContentType items_key_id: string | undefined enc_item_key: string version: ProtocolVersion + key_system_identifier: string | undefined + shared_vault_uuid: string | undefined /** @deprecated */ auth_hash?: string } -export type DecryptedParameters = { - uuid: string - content: C +export type EncryptedInputParameters = EncryptedOutputParameters & { + signatureData: PersistentSignatureData | undefined } export type ErrorDecryptingParameters = { @@ -24,18 +27,27 @@ export type ErrorDecryptingParameters = { } export function isErrorDecryptingParameters( - x: EncryptedParameters | DecryptedParameters | ErrorDecryptingParameters, + x: + | EncryptedOutputParameters + | DecryptedParameters + | ErrorDecryptingParameters + | DecryptedPayloadInterface + | EncryptedPayloadInterface, ): x is ErrorDecryptingParameters { return (x as ErrorDecryptingParameters).errorDecrypting } -export function encryptedParametersFromPayload(payload: EncryptedPayloadInterface): EncryptedParameters { +export function encryptedInputParametersFromPayload(payload: EncryptedPayloadInterface): EncryptedInputParameters { return { uuid: payload.uuid, content: payload.content, + content_type: payload.content_type, items_key_id: payload.items_key_id, enc_item_key: payload.enc_item_key as string, version: payload.version, auth_hash: payload.auth_hash, + key_system_identifier: payload.key_system_identifier, + shared_vault_uuid: payload.shared_vault_uuid, + signatureData: payload.signatureData, } } diff --git a/packages/encryption/src/Domain/Types/EncryptionAdditionalData.ts b/packages/encryption/src/Domain/Types/EncryptionAdditionalData.ts new file mode 100644 index 000000000..0f7b80689 --- /dev/null +++ b/packages/encryption/src/Domain/Types/EncryptionAdditionalData.ts @@ -0,0 +1,15 @@ +export type SigningData = { + signature: string + publicKey: string +} + +export type SymmetricItemAdditionalData = { + signingData?: SigningData | undefined +} + +export type AsymmetricItemAdditionalData = { + signingData: SigningData + senderPublicKey: string +} + +export type AdditionalData = SymmetricItemAdditionalData | AsymmetricItemAdditionalData diff --git a/packages/encryption/src/Domain/Types/ItemAuthenticatedData.ts b/packages/encryption/src/Domain/Types/ItemAuthenticatedData.ts index 92fc43079..b9bf704f3 100644 --- a/packages/encryption/src/Domain/Types/ItemAuthenticatedData.ts +++ b/packages/encryption/src/Domain/Types/ItemAuthenticatedData.ts @@ -1,6 +1,12 @@ import { ProtocolVersion } from '@standardnotes/common' +type UserUuid = string +type KeySystemIdentifier = string +type SharedVaultUuid = string + export type ItemAuthenticatedData = { - u: string + u: UserUuid v: ProtocolVersion + ksi?: KeySystemIdentifier + svu?: SharedVaultUuid } diff --git a/packages/encryption/src/Domain/Types/KeySystemItemsKeyAuthenticatedData.ts b/packages/encryption/src/Domain/Types/KeySystemItemsKeyAuthenticatedData.ts new file mode 100644 index 000000000..bbf05e44a --- /dev/null +++ b/packages/encryption/src/Domain/Types/KeySystemItemsKeyAuthenticatedData.ts @@ -0,0 +1,7 @@ +import { ItemAuthenticatedData } from './ItemAuthenticatedData' +import { KeySystemRootKeyParamsInterface } from '@standardnotes/models' + +/** Authenticated data for key system items key payloads */ +export type KeySystemItemsKeyAuthenticatedData = ItemAuthenticatedData & { + kp: KeySystemRootKeyParamsInterface +} diff --git a/packages/encryption/src/Domain/Types/KeySystemRootKeyEncryptedAuthenticatedData.ts b/packages/encryption/src/Domain/Types/KeySystemRootKeyEncryptedAuthenticatedData.ts new file mode 100644 index 000000000..fa374784e --- /dev/null +++ b/packages/encryption/src/Domain/Types/KeySystemRootKeyEncryptedAuthenticatedData.ts @@ -0,0 +1,4 @@ +import { ItemAuthenticatedData } from './ItemAuthenticatedData' + +/** Authenticated data for payloads encrypted with a key system root key */ +export type KeySystemRootKeyEncryptedAuthenticatedData = ItemAuthenticatedData diff --git a/packages/encryption/src/Domain/index.ts b/packages/encryption/src/Domain/index.ts index 617a5f8f6..15f668b51 100644 --- a/packages/encryption/src/Domain/index.ts +++ b/packages/encryption/src/Domain/index.ts @@ -1,28 +1,43 @@ export * from './Algorithm' export * from './Backups/BackupFileType' + export * from './Keys/ItemsKey/ItemsKey' export * from './Keys/ItemsKey/ItemsKeyMutator' export * from './Keys/ItemsKey/Registration' + +export * from './Keys/KeySystemItemsKey/KeySystemItemsKey' +export * from './Keys/KeySystemItemsKey/KeySystemItemsKeyMutator' +export * from './Keys/KeySystemItemsKey/Registration' + export * from './Keys/RootKey/Functions' export * from './Keys/RootKey/KeyParamsFunctions' export * from './Keys/RootKey/ProtocolVersionForKeyParams' export * from './Keys/RootKey/RootKey' export * from './Keys/RootKey/RootKeyParams' export * from './Keys/RootKey/ValidKeyParamsKeys' + export * from './Keys/Utils/KeyRecoveryStrings' + export * from './Operator/001/Operator001' export * from './Operator/002/Operator002' export * from './Operator/003/Operator003' export * from './Operator/004/Operator004' -export * from './Operator/005/Operator005' +export * from './Operator/004/V004AlgorithmHelpers' + export * from './Operator/Functions' -export * from './Operator/Operator' +export * from './Operator/OperatorInterface/OperatorInterface' export * from './Operator/OperatorManager' export * from './Operator/OperatorWrapper' +export * from './Operator/Types/PublicKeySet' +export * from './Operator/Types/AsymmetricSignatureVerificationDetachedResult' +export * from './Operator/Types/Types' + export * from './Service/Encryption/EncryptionProviderInterface' +export * from './Service/KeySystemKeyManagerInterface' export * from './Service/Functions' export * from './Service/RootKey/KeyMode' export * from './Service/RootKey/RootKeyServiceEvent' + export * from './Split/AbstractKeySplit' export * from './Split/EncryptionSplit' export * from './Split/EncryptionTypeSplit' @@ -30,8 +45,11 @@ export * from './Split/Functions' export * from './Split/KeyedDecryptionSplit' export * from './Split/KeyedEncryptionSplit' export * from './StandardException' + export * from './Types/EncryptedParameters' +export * from './Types/DecryptedParameters' export * from './Types/ItemAuthenticatedData' export * from './Types/LegacyAttachedData' export * from './Types/RootKeyEncryptedAuthenticatedData' + export * from './Username/PrivateUsername' diff --git a/packages/encryption/tsconfig.json b/packages/encryption/tsconfig.json index f3dac14ef..44c846c10 100644 --- a/packages/encryption/tsconfig.json +++ b/packages/encryption/tsconfig.json @@ -1,13 +1,11 @@ { - "extends": "../../node_modules/@standardnotes/config/src/tsconfig.json", + "extends": "../../UILib.tsconfig.json", "compilerOptions": { "skipLibCheck": true, "rootDir": "./src", "outDir": "./dist", + "noEmit": true }, - "include": [ - "src/**/*" - ], - "references": [], - "exclude": ["**/*.spec.ts", "dist", "node_modules"] + "include": ["src/**/*"], + "exclude": ["node_modules"] } diff --git a/packages/features/package.json b/packages/features/package.json index 299ab07e7..2cde0e343 100644 --- a/packages/features/package.json +++ b/packages/features/package.json @@ -25,7 +25,7 @@ "test": "jest" }, "dependencies": { - "@standardnotes/common": "^1.46.6", + "@standardnotes/common": "^1.48.3", "@standardnotes/domain-core": "^1.12.0", "@standardnotes/security": "^1.7.6", "reflect-metadata": "^0.1.13" diff --git a/packages/filepicker/package.json b/packages/filepicker/package.json index a099deeac..057b72119 100644 --- a/packages/filepicker/package.json +++ b/packages/filepicker/package.json @@ -26,7 +26,7 @@ "typescript": "*" }, "dependencies": { - "@standardnotes/common": "^1.46.6", + "@standardnotes/common": "^1.48.3", "@standardnotes/files": "workspace:*", "@standardnotes/utils": "workspace:*", "@types/wicg-file-system-access": "^2020.9.5", diff --git a/packages/files/package.json b/packages/files/package.json index a90e6945a..bffda4dd6 100644 --- a/packages/files/package.json +++ b/packages/files/package.json @@ -28,7 +28,7 @@ "typescript": "*" }, "dependencies": { - "@standardnotes/common": "^1.46.4", + "@standardnotes/common": "^1.48.3", "@standardnotes/encryption": "workspace:*", "@standardnotes/models": "workspace:*", "@standardnotes/responses": "workspace:*", diff --git a/packages/files/src/Domain/Api/DownloadFileParams.ts b/packages/files/src/Domain/Api/DownloadFileParams.ts new file mode 100644 index 000000000..14b96ed04 --- /dev/null +++ b/packages/files/src/Domain/Api/DownloadFileParams.ts @@ -0,0 +1,11 @@ +import { FileContent } from '@standardnotes/models' +import { FileOwnershipType } from './FileOwnershipType' + +export type DownloadFileParams = { + file: { encryptedChunkSizes: FileContent['encryptedChunkSizes'] } + chunkIndex: number + valetToken: string + ownershipType: FileOwnershipType + contentRangeStart: number + onBytesReceived: (bytes: Uint8Array) => Promise +} diff --git a/packages/files/src/Domain/Api/FileOwnershipType.ts b/packages/files/src/Domain/Api/FileOwnershipType.ts new file mode 100644 index 000000000..acbb29641 --- /dev/null +++ b/packages/files/src/Domain/Api/FileOwnershipType.ts @@ -0,0 +1 @@ +export type FileOwnershipType = 'user' | 'shared-vault' diff --git a/packages/files/src/Domain/Api/FilesApiInterface.ts b/packages/files/src/Domain/Api/FilesApiInterface.ts index 07bae17ab..f09c72785 100644 --- a/packages/files/src/Domain/Api/FilesApiInterface.ts +++ b/packages/files/src/Domain/Api/FilesApiInterface.ts @@ -1,28 +1,38 @@ -import { StartUploadSessionResponse, HttpResponse, ClientDisplayableError } from '@standardnotes/responses' -import { FileContent } from '@standardnotes/models' +import { + StartUploadSessionResponse, + HttpResponse, + ClientDisplayableError, + ValetTokenOperation, +} from '@standardnotes/responses' +import { DownloadFileParams } from './DownloadFileParams' +import { FileOwnershipType } from './FileOwnershipType' export interface FilesApiInterface { - startUploadSession(apiToken: string): Promise> - - uploadFileBytes(apiToken: string, chunkId: number, encryptedBytes: Uint8Array): Promise - - closeUploadSession(apiToken: string): Promise - - downloadFile( - file: { encryptedChunkSizes: FileContent['encryptedChunkSizes'] }, - chunkIndex: number, - apiToken: string, - contentRangeStart: number, - onBytesReceived: (bytes: Uint8Array) => Promise, - ): Promise - - deleteFile(apiToken: string): Promise - - createFileValetToken( + createUserFileValetToken( remoteIdentifier: string, - operation: 'write' | 'read' | 'delete', + operation: ValetTokenOperation, unencryptedFileSize?: number, ): Promise - getFilesDownloadUrl(): string + startUploadSession( + valetToken: string, + ownershipType: FileOwnershipType, + ): Promise> + + uploadFileBytes( + valetToken: string, + ownershipType: FileOwnershipType, + chunkId: number, + encryptedBytes: Uint8Array, + ): Promise + + closeUploadSession(valetToken: string, ownershipType: FileOwnershipType): Promise + + downloadFile(params: DownloadFileParams): Promise + + moveFile(valetToken: string): Promise + + deleteFile(valetToken: string, ownershipType: FileOwnershipType): Promise + + getFilesDownloadUrl(ownershipType: FileOwnershipType): string } diff --git a/packages/files/src/Domain/Operations/DownloadAndDecrypt.spec.ts b/packages/files/src/Domain/Operations/DownloadAndDecrypt.spec.ts index ba26d4304..372499dbd 100644 --- a/packages/files/src/Domain/Operations/DownloadAndDecrypt.spec.ts +++ b/packages/files/src/Domain/Operations/DownloadAndDecrypt.spec.ts @@ -9,10 +9,12 @@ describe('download and decrypt', () => { let apiService: FilesApiInterface let operation: DownloadAndDecryptFileOperation let file: { + uuid: string encryptedChunkSizes: FileContent['encryptedChunkSizes'] encryptionHeader: FileContent['encryptionHeader'] remoteIdentifier: FileContent['remoteIdentifier'] key: FileContent['key'] + shared_vault_uuid: string | undefined } let crypto: PureCryptoInterface @@ -26,16 +28,16 @@ describe('download and decrypt', () => { apiService.downloadFile = jest .fn() .mockImplementation( - ( - _file: string, - _chunkIndex: number, - _apiToken: string, - _rangeStart: number, - onBytesReceived: (bytes: Uint8Array) => void, - ) => { + (params: { + _file: string + _chunkIndex: number + _apiToken: string + _rangeStart: number + onBytesReceived: (bytes: Uint8Array) => void + }) => { const receiveFile = async () => { for (let i = 0; i < NumChunks; i++) { - onBytesReceived(chunkOfSize(size)) + params.onBytesReceived(chunkOfSize(size)) await sleep(100, false) } @@ -50,7 +52,7 @@ describe('download and decrypt', () => { beforeEach(() => { apiService = {} as jest.Mocked - apiService.createFileValetToken = jest.fn() + apiService.createUserFileValetToken = jest.fn() downloadChunksOfSize(5) crypto = {} as jest.Mocked @@ -62,17 +64,19 @@ describe('download and decrypt', () => { crypto.xchacha20StreamDecryptorPush = jest.fn().mockReturnValue({ message: new Uint8Array([0xaa]), tag: 0 }) file = { + uuid: '123', encryptedChunkSizes: [100_000], remoteIdentifier: '123', key: 'secret', encryptionHeader: 'some-header', + shared_vault_uuid: undefined, } }) it('run should resolve when operation is complete', async () => { let receivedBytes = new Uint8Array() - operation = new DownloadAndDecryptFileOperation(file, crypto, apiService) + operation = new DownloadAndDecryptFileOperation(file, crypto, apiService, 'own') await operation.run(async (result) => { if (result) { @@ -87,15 +91,17 @@ describe('download and decrypt', () => { it('should correctly report progress', async () => { file = { + uuid: '123', encryptedChunkSizes: [100_000, 200_000, 200_000], remoteIdentifier: '123', key: 'secret', encryptionHeader: 'some-header', + shared_vault_uuid: undefined, } downloadChunksOfSize(100_000) - operation = new DownloadAndDecryptFileOperation(file, crypto, apiService) + operation = new DownloadAndDecryptFileOperation(file, crypto, apiService, 'own') const progress: FileDownloadProgress = await new Promise((resolve) => { // eslint-disable-next-line @typescript-eslint/require-await diff --git a/packages/files/src/Domain/Operations/DownloadAndDecrypt.ts b/packages/files/src/Domain/Operations/DownloadAndDecrypt.ts index a2f644332..38504b81c 100644 --- a/packages/files/src/Domain/Operations/DownloadAndDecrypt.ts +++ b/packages/files/src/Domain/Operations/DownloadAndDecrypt.ts @@ -21,6 +21,8 @@ export class DownloadAndDecryptFileOperation { constructor( private readonly file: { + uuid: string + shared_vault_uuid: string | undefined encryptedChunkSizes: FileContent['encryptedChunkSizes'] encryptionHeader: FileContent['encryptionHeader'] remoteIdentifier: FileContent['remoteIdentifier'] @@ -28,8 +30,9 @@ export class DownloadAndDecryptFileOperation { }, private readonly crypto: PureCryptoInterface, private readonly api: FilesApiInterface, + valetToken: string, ) { - this.downloader = new FileDownloader(this.file, this.api) + this.downloader = new FileDownloader(this.file, this.api, valetToken) } private createDecryptor(): FileDecryptor { diff --git a/packages/files/src/Domain/Operations/EncryptAndUpload.ts b/packages/files/src/Domain/Operations/EncryptAndUpload.ts index 71af93a31..6af4fdcc9 100644 --- a/packages/files/src/Domain/Operations/EncryptAndUpload.ts +++ b/packages/files/src/Domain/Operations/EncryptAndUpload.ts @@ -3,7 +3,7 @@ import { FileUploadResult } from '../Types/FileUploadResult' import { FileUploader } from '../UseCase/FileUploader' import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { FileEncryptor } from '../UseCase/FileEncryptor' -import { FileContent } from '@standardnotes/models' +import { FileContent, VaultListingInterface } from '@standardnotes/models' import { FilesApiInterface } from '../Api/FilesApiInterface' export class EncryptAndUploadFileOperation { @@ -22,9 +22,10 @@ export class EncryptAndUploadFileOperation { key: FileContent['key'] remoteIdentifier: FileContent['remoteIdentifier'] }, - private apiToken: string, + private valetToken: string, private crypto: PureCryptoInterface, private api: FilesApiInterface, + public readonly vault?: VaultListingInterface, ) { this.encryptor = new FileEncryptor(file, this.crypto) this.uploader = new FileUploader(this.api) @@ -32,8 +33,8 @@ export class EncryptAndUploadFileOperation { this.encryptionHeader = this.encryptor.initializeHeader() } - public getApiToken(): string { - return this.apiToken + public getValetToken(): string { + return this.valetToken } public getProgress(): FileUploadProgress { @@ -79,7 +80,12 @@ export class EncryptAndUploadFileOperation { } private async uploadBytes(encryptedBytes: Uint8Array, chunkId: number): Promise { - const success = await this.uploader.uploadBytes(encryptedBytes, chunkId, this.apiToken) + const success = await this.uploader.uploadBytes( + encryptedBytes, + this.vault && this.vault.sharing ? 'shared-vault' : 'user', + chunkId, + this.valetToken, + ) return success } diff --git a/packages/files/src/Domain/Service/FilesClientInterface.ts b/packages/files/src/Domain/Service/FilesClientInterface.ts index 99c41f104..6f7b462c4 100644 --- a/packages/files/src/Domain/Service/FilesClientInterface.ts +++ b/packages/files/src/Domain/Service/FilesClientInterface.ts @@ -1,5 +1,5 @@ import { EncryptAndUploadFileOperation } from '../Operations/EncryptAndUpload' -import { FileItem, FileMetadata } from '@standardnotes/models' +import { FileItem, FileMetadata, VaultListingInterface, SharedVaultListingInterface } from '@standardnotes/models' import { ClientDisplayableError } from '@standardnotes/responses' import { FileDownloadProgress } from '../Types/FileDownloadProgress' import { FileSystemApi } from '../Api/FileSystemApi' @@ -8,15 +8,18 @@ import { FileSystemNoSelection } from '../Api/FileSystemNoSelection' import { FileBackupMetadataFile } from '../Device/FileBackupMetadataFile' export interface FilesClientInterface { - beginNewFileUpload(sizeInBytes: number): Promise + minimumChunkSize(): number + beginNewFileUpload( + sizeInBytes: number, + vault?: VaultListingInterface, + ): Promise pushBytesForUpload( operation: EncryptAndUploadFileOperation, bytes: Uint8Array, chunkId: number, isFinalChunk: boolean, ): Promise - finishUpload( operation: EncryptAndUploadFileOperation, fileMetadata: FileMetadata, @@ -29,20 +32,21 @@ export interface FilesClientInterface { deleteFile(file: FileItem): Promise - minimumChunkSize(): number - - isFileNameFileBackupRelated(name: string): 'metadata' | 'binary' | false - - decryptBackupMetadataFile(metdataFile: FileBackupMetadataFile): Promise + moveFileToSharedVault( + file: FileItem, + sharedVault: SharedVaultListingInterface, + ): Promise + moveFileOutOfSharedVault(file: FileItem): Promise selectFile(fileSystem: FileSystemApi): Promise + isFileNameFileBackupRelated(name: string): 'metadata' | 'binary' | false + decryptBackupMetadataFile(metdataFile: FileBackupMetadataFile): Promise readBackupFileAndSaveDecrypted( fileHandle: FileHandleRead, file: FileItem, fileSystem: FileSystemApi, ): Promise<'success' | 'aborted' | 'failed'> - readBackupFileBytesDecrypted( fileHandle: FileHandleRead, file: FileItem, diff --git a/packages/files/src/Domain/UseCase/FileDownloader.spec.ts b/packages/files/src/Domain/UseCase/FileDownloader.spec.ts index 2ef9d3cca..abd941ea4 100644 --- a/packages/files/src/Domain/UseCase/FileDownloader.spec.ts +++ b/packages/files/src/Domain/UseCase/FileDownloader.spec.ts @@ -6,6 +6,8 @@ describe('file downloader', () => { let apiService: FilesApiInterface let downloader: FileDownloader let file: { + uuid: string + shared_vault_uuid: string | undefined encryptedChunkSizes: FileContent['encryptedChunkSizes'] remoteIdentifier: FileContent['remoteIdentifier'] } @@ -14,20 +16,20 @@ describe('file downloader', () => { beforeEach(() => { apiService = {} as jest.Mocked - apiService.createFileValetToken = jest.fn() + apiService.createUserFileValetToken = jest.fn() apiService.downloadFile = jest .fn() .mockImplementation( - ( - _file: string, - _chunkIndex: number, - _apiToken: string, - _rangeStart: number, - onBytesReceived: (bytes: Uint8Array) => void, - ) => { + (params: { + _file: string + _chunkIndex: number + _apiToken: string + _rangeStart: number + onBytesReceived: (bytes: Uint8Array) => void + }) => { return new Promise((resolve) => { for (let i = 0; i < numChunks; i++) { - onBytesReceived(Uint8Array.from([0xaa])) + params.onBytesReceived(Uint8Array.from([0xaa])) } resolve() @@ -36,6 +38,8 @@ describe('file downloader', () => { ) file = { + uuid: '123', + shared_vault_uuid: undefined, encryptedChunkSizes: [100_000], remoteIdentifier: '123', } @@ -44,7 +48,7 @@ describe('file downloader', () => { it('should pass back bytes as they are received', async () => { let receivedBytes = new Uint8Array() - downloader = new FileDownloader(file, apiService) + downloader = new FileDownloader(file, apiService, 'valet-token') expect(receivedBytes.length).toBe(0) diff --git a/packages/files/src/Domain/UseCase/FileDownloader.ts b/packages/files/src/Domain/UseCase/FileDownloader.ts index ea1c3f93c..424d5dbda 100644 --- a/packages/files/src/Domain/UseCase/FileDownloader.ts +++ b/packages/files/src/Domain/UseCase/FileDownloader.ts @@ -21,10 +21,13 @@ export class FileDownloader { constructor( private file: { + uuid: string + shared_vault_uuid: string | undefined encryptedChunkSizes: FileContent['encryptedChunkSizes'] remoteIdentifier: FileContent['remoteIdentifier'] }, private readonly api: FilesApiInterface, + private readonly valetToken: string, ) {} private getProgress(): FileDownloadProgress { @@ -40,22 +43,10 @@ export class FileDownloader { } public async run(onEncryptedBytes: OnEncryptedBytes): Promise { - const tokenResult = await this.getValetToken() - - if (tokenResult instanceof ClientDisplayableError) { - return tokenResult - } - - return this.performDownload(tokenResult, onEncryptedBytes) + return this.performDownload(onEncryptedBytes) } - private async getValetToken(): Promise { - const tokenResult = await this.api.createFileValetToken(this.file.remoteIdentifier, 'read') - - return tokenResult - } - - private async performDownload(valetToken: string, onEncryptedBytes: OnEncryptedBytes): Promise { + private async performDownload(onEncryptedBytes: OnEncryptedBytes): Promise { const chunkIndex = 0 const startRange = 0 @@ -69,7 +60,14 @@ export class FileDownloader { await onEncryptedBytes(bytes, this.getProgress(), this.abort) } - const downloadPromise = this.api.downloadFile(this.file, chunkIndex, valetToken, startRange, onRemoteBytesReceived) + const downloadPromise = this.api.downloadFile({ + file: this.file, + chunkIndex, + valetToken: this.valetToken, + contentRangeStart: startRange, + onBytesReceived: onRemoteBytesReceived, + ownershipType: this.file.shared_vault_uuid ? 'shared-vault' : 'user', + }) const result = await Promise.race([this.abortDeferred.promise, downloadPromise]) diff --git a/packages/files/src/Domain/UseCase/FileUploader.spec.ts b/packages/files/src/Domain/UseCase/FileUploader.spec.ts index 4a31d101e..353b2f79d 100644 --- a/packages/files/src/Domain/UseCase/FileUploader.spec.ts +++ b/packages/files/src/Domain/UseCase/FileUploader.spec.ts @@ -2,7 +2,7 @@ import { FilesApiInterface } from '../Api/FilesApiInterface' import { FileUploader } from './FileUploader' describe('file uploader', () => { - let apiService + let apiService: FilesApiInterface let uploader: FileUploader beforeEach(() => { @@ -14,7 +14,7 @@ describe('file uploader', () => { it('should return true when a chunk is uploaded', async () => { const bytes = new Uint8Array() - const success = await uploader.uploadBytes(bytes, 2, 'api-token') + const success = await uploader.uploadBytes(bytes, 'user', 2, 'api-token') expect(success).toEqual(true) }) diff --git a/packages/files/src/Domain/UseCase/FileUploader.ts b/packages/files/src/Domain/UseCase/FileUploader.ts index d1c3acaa2..1141fe839 100644 --- a/packages/files/src/Domain/UseCase/FileUploader.ts +++ b/packages/files/src/Domain/UseCase/FileUploader.ts @@ -1,10 +1,16 @@ +import { FileOwnershipType } from '../Api/FileOwnershipType' import { FilesApiInterface } from '../Api/FilesApiInterface' export class FileUploader { constructor(private apiService: FilesApiInterface) {} - public async uploadBytes(encryptedBytes: Uint8Array, chunkId: number, apiToken: string): Promise { - const result = await this.apiService.uploadFileBytes(apiToken, chunkId, encryptedBytes) + public async uploadBytes( + encryptedBytes: Uint8Array, + ownershipType: FileOwnershipType, + chunkId: number, + apiToken: string, + ): Promise { + const result = await this.apiService.uploadFileBytes(apiToken, ownershipType, chunkId, encryptedBytes) return result } diff --git a/packages/files/src/Domain/index.ts b/packages/files/src/Domain/index.ts index 405e6c28b..07f8bcce6 100644 --- a/packages/files/src/Domain/index.ts +++ b/packages/files/src/Domain/index.ts @@ -5,6 +5,9 @@ export * from './Api/FilesApiInterface' export * from './Api/FileSystemApi' export * from './Api/FileSystemNoSelection' export * from './Api/FileSystemResult' +export * from './Api/DownloadFileParams' +export * from './Api/FileOwnershipType' + export * from './Cache/FileMemoryCache' export * from './Chunker/ByteChunker' export * from './Chunker/OnChunkCallback' diff --git a/packages/icons/src/Icons/ic-group.svg b/packages/icons/src/Icons/ic-group.svg new file mode 100644 index 000000000..8e3e3ddd7 --- /dev/null +++ b/packages/icons/src/Icons/ic-group.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/icons/src/Icons/index.ts b/packages/icons/src/Icons/index.ts index 53b9cb5e6..f83440a96 100644 --- a/packages/icons/src/Icons/index.ts +++ b/packages/icons/src/Icons/index.ts @@ -91,6 +91,7 @@ import FullscreenExitIcon from './ic-fullscreen-exit.svg' import FullscreenIcon from './ic-fullscreen.svg' import GiftOutlineIcon from './ic-gift-outline.svg' import GoogleKeepIcon from './ic-gkeep.svg' +import GroupIcon from './ic-group.svg' import HashtagFilledIcon from './ic-hashtag-filled.svg' import HashtagIcon from './ic-hashtag.svg' import HashtagOffIcon from './ic-hashtag-off.svg' @@ -301,6 +302,7 @@ export { FullscreenExitIcon, FullscreenIcon, GiftOutlineIcon, + GroupIcon, HashtagFilledIcon, HashtagIcon, HashtagOffIcon, diff --git a/packages/mobile/ios/Podfile.lock b/packages/mobile/ios/Podfile.lock index ac3cd08b2..a3217a32a 100644 --- a/packages/mobile/ios/Podfile.lock +++ b/packages/mobile/ios/Podfile.lock @@ -637,7 +637,7 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - boost: 57d2868c099736d80fcd648bf211b4431e51a558 + boost: a7c83b31436843459a1961bfd74b96033dc77234 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: 60195509584153283780abdac5569feffb8f08cc @@ -658,7 +658,7 @@ SPEC CHECKSUMS: MMKV: 9c4663aa7ca255d478ff10f2f5cb7d17c1651ccd MMKVCore: 89f5c8a66bba2dcd551779dea4d412eeec8ff5bb OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c - RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 + RCT-Folly: 0080d0a6ebf2577475bda044aa59e2ca1f909cda RCTRequired: bec48f07daf7bcdc2655a0cde84e07d24d2a9e2a RCTTypeSafety: 171394eebacf71e1cfad79dbfae7ee8fc16ca80a React: d7433ccb6a8c36e4cbed59a73c0700fc83c3e98a diff --git a/packages/mobile/ios/StandardNotes/Info.plist b/packages/mobile/ios/StandardNotes/Info.plist index ae73e210c..569f5dff8 100644 --- a/packages/mobile/ios/StandardNotes/Info.plist +++ b/packages/mobile/ios/StandardNotes/Info.plist @@ -66,7 +66,7 @@ NSCameraUsageDescription - Camera is optionally used to upload images and videos and scan QR codes using the TokenVault extension. + Camera is optionally used to upload images and videos and scan QR codes using the Authenticator extension. NSFaceIDUsageDescription Face ID is required to unlock your notes. NSLocationAlwaysUsageDescription diff --git a/packages/mobile/src/Lib/Database/Database.ts b/packages/mobile/src/Lib/Database/Database.ts index de5b14713..35f1d1f56 100644 --- a/packages/mobile/src/Lib/Database/Database.ts +++ b/packages/mobile/src/Lib/Database/Database.ts @@ -84,17 +84,31 @@ export class Database implements DatabaseInterface { metadataItems = this.metadataStore.runMigration(allEntries) } - const sorted = GetSortedPayloadsByPriority(metadataItems, options) + const { + itemsKeyPayloads, + keySystemRootKeyPayloads, + keySystemItemsKeyPayloads, + contentTypePriorityPayloads, + remainingPayloads, + } = GetSortedPayloadsByPriority(metadataItems, options) const itemsKeysChunk: DatabaseKeysLoadChunk = { - keys: sorted.itemsKeyPayloads.map((item) => this.databaseKeyForPayloadId(item.uuid)), + keys: itemsKeyPayloads.map((item) => this.databaseKeyForPayloadId(item.uuid)), + } + + const keySystemRootKeysChunk: DatabaseKeysLoadChunk = { + keys: keySystemRootKeyPayloads.map((item) => this.databaseKeyForPayloadId(item.uuid)), + } + + const keySystemItemsKeysChunk: DatabaseKeysLoadChunk = { + keys: keySystemItemsKeyPayloads.map((item) => this.databaseKeyForPayloadId(item.uuid)), } const contentTypePriorityChunk: DatabaseKeysLoadChunk = { - keys: sorted.contentTypePriorityPayloads.map((item) => this.databaseKeyForPayloadId(item.uuid)), + keys: contentTypePriorityPayloads.map((item) => this.databaseKeyForPayloadId(item.uuid)), } - const remainingKeys = sorted.remainingPayloads.map((item) => this.databaseKeyForPayloadId(item.uuid)) + const remainingKeys = remainingPayloads.map((item) => this.databaseKeyForPayloadId(item.uuid)) const remainingKeysChunks: DatabaseKeysLoadChunk[] = [] for (let i = 0; i < remainingKeys.length; i += options.batchSize) { @@ -106,9 +120,11 @@ export class Database implements DatabaseInterface { const result: DatabaseKeysLoadChunkResponse = { keys: { itemsKeys: itemsKeysChunk, + keySystemRootKeys: keySystemRootKeysChunk, + keySystemItemsKeys: keySystemItemsKeysChunk, remainingChunks: [contentTypePriorityChunk, ...remainingKeysChunks], }, - remainingChunksItemCount: sorted.contentTypePriorityPayloads.length + sorted.remainingPayloads.length, + remainingChunksItemCount: contentTypePriorityPayloads.length + remainingPayloads.length, } return result diff --git a/packages/models/package.json b/packages/models/package.json index b1ffc717c..330a96d95 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -22,9 +22,10 @@ "test": "jest" }, "dependencies": { - "@standardnotes/common": "^1.46.6", + "@standardnotes/common": "^1.48.3", "@standardnotes/features": "workspace:*", "@standardnotes/responses": "workspace:*", + "@standardnotes/sncrypto-common": "workspace:^", "@standardnotes/utils": "workspace:^", "lodash": "^4.17.21" }, diff --git a/packages/models/src/Domain/Abstract/Contextual/ContextPayload.ts b/packages/models/src/Domain/Abstract/Contextual/ContextPayload.ts index a3cd11887..256706a19 100644 --- a/packages/models/src/Domain/Abstract/Contextual/ContextPayload.ts +++ b/packages/models/src/Domain/Abstract/Contextual/ContextPayload.ts @@ -6,4 +6,9 @@ export interface ContextPayload { content_type: ContentType content: C | string | undefined deleted: boolean + + user_uuid?: string + key_system_identifier?: string | undefined + shared_vault_uuid?: string | undefined + last_edited_by_uuid?: string } diff --git a/packages/models/src/Domain/Abstract/Contextual/FilteredServerItem.ts b/packages/models/src/Domain/Abstract/Contextual/FilteredServerItem.ts index 54c038db7..57c402664 100644 --- a/packages/models/src/Domain/Abstract/Contextual/FilteredServerItem.ts +++ b/packages/models/src/Domain/Abstract/Contextual/FilteredServerItem.ts @@ -5,7 +5,7 @@ export interface FilteredServerItem extends ServerItemResponse { __passed_filter__: true } -export function CreateFilteredServerItem(item: ServerItemResponse): FilteredServerItem { +function CreateFilteredServerItem(item: ServerItemResponse): FilteredServerItem { return { ...item, __passed_filter__: true, diff --git a/packages/models/src/Domain/Abstract/Contextual/Functions.ts b/packages/models/src/Domain/Abstract/Contextual/Functions.ts index 3fd55da51..0dc5e276e 100644 --- a/packages/models/src/Domain/Abstract/Contextual/Functions.ts +++ b/packages/models/src/Domain/Abstract/Contextual/Functions.ts @@ -19,6 +19,8 @@ export function CreateEncryptedBackupFileContextPayload( updated_at_timestamp: fromPayload.updated_at_timestamp, updated_at: fromPayload.updated_at, uuid: fromPayload.uuid, + key_system_identifier: fromPayload.key_system_identifier, + shared_vault_uuid: fromPayload.shared_vault_uuid, } } @@ -35,5 +37,7 @@ export function CreateDecryptedBackupFileContextPayload( updated_at_timestamp: fromPayload.updated_at_timestamp, updated_at: fromPayload.updated_at, uuid: fromPayload.uuid, + key_system_identifier: fromPayload.key_system_identifier, + shared_vault_uuid: fromPayload.shared_vault_uuid, } } diff --git a/packages/models/src/Domain/Abstract/Contextual/LocalStorage.ts b/packages/models/src/Domain/Abstract/Contextual/LocalStorage.ts index e9c9ee6b1..2fde7044d 100644 --- a/packages/models/src/Domain/Abstract/Contextual/LocalStorage.ts +++ b/packages/models/src/Domain/Abstract/Contextual/LocalStorage.ts @@ -3,6 +3,7 @@ import { ItemContent } from '../Content/ItemContent' import { DecryptedPayloadInterface, DeletedPayloadInterface, EncryptedPayloadInterface } from '../Payload' import { useBoolean } from '@standardnotes/utils' import { EncryptedTransferPayload, isEncryptedTransferPayload } from '../TransferPayload' +import { PersistentSignatureData } from '../../Runtime/Encryption/PersistentSignatureData' export function isEncryptedLocalStoragePayload( p: LocalStorageEncryptedContextualPayload | LocalStorageDecryptedContextualPayload, @@ -25,6 +26,7 @@ export interface LocalStorageEncryptedContextualPayload extends ContextPayload { updated_at_timestamp: number updated_at: Date waitingForKey: boolean + signatureData?: PersistentSignatureData } export interface LocalStorageDecryptedContextualPayload extends ContextPayload { @@ -36,6 +38,7 @@ export interface LocalStorageDecryptedContextualPayload diff --git a/packages/models/src/Domain/Abstract/Item/Implementations/DecryptedItem.ts b/packages/models/src/Domain/Abstract/Item/Implementations/DecryptedItem.ts index 8ae00ea3d..e1736e015 100644 --- a/packages/models/src/Domain/Abstract/Item/Implementations/DecryptedItem.ts +++ b/packages/models/src/Domain/Abstract/Item/Implementations/DecryptedItem.ts @@ -50,7 +50,7 @@ export class DecryptedItem return this.payload.content.references || [] } - public isReferencingItem(item: DecryptedItemInterface): boolean { + public isReferencingItem(item: { uuid: string }): boolean { return this.references.find((r) => r.uuid === item.uuid) != undefined } diff --git a/packages/models/src/Domain/Abstract/Item/Implementations/GenericItem.ts b/packages/models/src/Domain/Abstract/Item/Implementations/GenericItem.ts index 327478860..ba974a484 100644 --- a/packages/models/src/Domain/Abstract/Item/Implementations/GenericItem.ts +++ b/packages/models/src/Domain/Abstract/Item/Implementations/GenericItem.ts @@ -10,6 +10,7 @@ import { SingletonStrategy } from '../Types/SingletonStrategy' import { PayloadInterface } from '../../Payload/Interfaces/PayloadInterface' import { HistoryEntryInterface } from '../../../Runtime/History/HistoryEntryInterface' import { isDecryptedItem, isDeletedItem, isEncryptedErroredItem } from '../Interfaces/TypeCheck' +import { PersistentSignatureData } from '../../../Runtime/Encryption/PersistentSignatureData' export abstract class GenericItem

implements ItemInterface

{ payload: P @@ -43,6 +44,26 @@ export abstract class GenericItem

return this.payload.created_at } + get key_system_identifier(): string | undefined { + return this.payload.key_system_identifier + } + + get user_uuid(): string | undefined { + return this.payload.user_uuid + } + + get shared_vault_uuid(): string | undefined { + return this.payload.shared_vault_uuid + } + + get last_edited_by_uuid(): string | undefined { + return this.payload.last_edited_by_uuid + } + + get signatureData(): PersistentSignatureData | undefined { + return this.payload.signatureData + } + /** * The date timestamp the server set for this item upon it being synced * Undefined if never synced to a remote server. diff --git a/packages/models/src/Domain/Abstract/Item/Interfaces/DecryptedItem.ts b/packages/models/src/Domain/Abstract/Item/Interfaces/DecryptedItem.ts index 68ed08d10..91699bbb3 100644 --- a/packages/models/src/Domain/Abstract/Item/Interfaces/DecryptedItem.ts +++ b/packages/models/src/Domain/Abstract/Item/Interfaces/DecryptedItem.ts @@ -33,7 +33,7 @@ export interface DecryptedItemInterface payloadRepresentation(override?: Partial>): DecryptedPayloadInterface - isReferencingItem(item: DecryptedItemInterface): boolean + isReferencingItem(item: { uuid: string }): boolean getDomainData(domain: typeof ComponentDataDomain | typeof DefaultAppDomain): undefined | Record diff --git a/packages/models/src/Domain/Abstract/Item/Interfaces/ItemInterface.ts b/packages/models/src/Domain/Abstract/Item/Interfaces/ItemInterface.ts index 393cf7ac5..3e399aecf 100644 --- a/packages/models/src/Domain/Abstract/Item/Interfaces/ItemInterface.ts +++ b/packages/models/src/Domain/Abstract/Item/Interfaces/ItemInterface.ts @@ -5,6 +5,7 @@ import { PredicateInterface } from '../../../Runtime/Predicate/Interface' import { HistoryEntryInterface } from '../../../Runtime/History' import { ConflictStrategy } from '../Types/ConflictStrategy' import { SingletonStrategy } from '../Types/SingletonStrategy' +import { PersistentSignatureData } from '../../../Runtime/Encryption/PersistentSignatureData' export interface ItemInterface

{ payload: P @@ -14,6 +15,11 @@ export interface ItemInterface

{ readonly updatedAtString?: string uuid: string + get key_system_identifier(): string | undefined + get user_uuid(): string | undefined + get shared_vault_uuid(): string | undefined + get last_edited_by_uuid(): string | undefined + get signatureData(): PersistentSignatureData | undefined content_type: ContentType created_at: Date diff --git a/packages/models/src/Domain/Abstract/Item/Mutator/DecryptedItemMutator.ts b/packages/models/src/Domain/Abstract/Item/Mutator/DecryptedItemMutator.ts index deb44e969..d0732847e 100644 --- a/packages/models/src/Domain/Abstract/Item/Mutator/DecryptedItemMutator.ts +++ b/packages/models/src/Domain/Abstract/Item/Mutator/DecryptedItemMutator.ts @@ -10,13 +10,13 @@ import { DecryptedPayloadInterface } from '../../Payload/Interfaces/DecryptedPay import { ItemInterface } from '../Interfaces/ItemInterface' import { getIncrementedDirtyIndex } from '../../../Runtime/DirtyCounter/DirtyCounter' -export class DecryptedItemMutator extends ItemMutator< - DecryptedPayloadInterface, - DecryptedItemInterface -> { +export class DecryptedItemMutator< + C extends ItemContent = ItemContent, + I extends DecryptedItemInterface = DecryptedItemInterface, +> extends ItemMutator, I> { protected mutableContent: C - constructor(item: DecryptedItemInterface, type: MutationType) { + constructor(item: I, type: MutationType) { super(item, type) const mutableCopy = Copy(this.immutablePayload.content) @@ -43,6 +43,8 @@ export class DecryptedItemMutator extends I content: this.mutableContent, dirty: true, dirtyIndex: getIncrementedDirtyIndex(), + signatureData: undefined, + last_edited_by_uuid: undefined, }) return result diff --git a/packages/models/src/Domain/Abstract/Item/Mutator/ItemMutator.ts b/packages/models/src/Domain/Abstract/Item/Mutator/ItemMutator.ts index 2214a9d70..5594d1a5d 100644 --- a/packages/models/src/Domain/Abstract/Item/Mutator/ItemMutator.ts +++ b/packages/models/src/Domain/Abstract/Item/Mutator/ItemMutator.ts @@ -3,6 +3,8 @@ import { PayloadInterface } from '../../Payload' import { ItemInterface } from '../Interfaces/ItemInterface' import { TransferPayload } from '../../TransferPayload' import { getIncrementedDirtyIndex } from '../../../Runtime/DirtyCounter/DirtyCounter' +import { KeySystemIdentifier } from '../../../Syncable/KeySystemRootKey/KeySystemIdentifier' +import { ContentTypeUsesRootKeyEncryption } from '../../../Runtime/Encryption/ContentTypeUsesRootKeyEncryption' /** * An item mutator takes in an item, and an operation, and returns the resulting payload. @@ -51,6 +53,26 @@ export class ItemMutator< }) } + public set key_system_identifier(keySystemIdentifier: KeySystemIdentifier | undefined) { + if (ContentTypeUsesRootKeyEncryption(this.immutableItem.content_type)) { + throw new Error('Cannot set key_system_identifier on a root key encrypted item') + } + + this.immutablePayload = this.immutablePayload.copy({ + key_system_identifier: keySystemIdentifier, + }) + } + + public set shared_vault_uuid(sharedVaultUuid: string | undefined) { + if (ContentTypeUsesRootKeyEncryption(this.immutableItem.content_type)) { + throw new Error('Cannot set shared_vault_uuid on a root key encrypted item') + } + + this.immutablePayload = this.immutablePayload.copy({ + shared_vault_uuid: sharedVaultUuid, + }) + } + public set errorDecrypting(_: boolean) { throw Error('This method is no longer implemented') } diff --git a/packages/models/src/Domain/Abstract/Payload/Implementations/PurePayload.ts b/packages/models/src/Domain/Abstract/Payload/Implementations/PurePayload.ts index ccd5a6830..d30c87e32 100644 --- a/packages/models/src/Domain/Abstract/Payload/Implementations/PurePayload.ts +++ b/packages/models/src/Domain/Abstract/Payload/Implementations/PurePayload.ts @@ -5,6 +5,8 @@ import { PayloadSource } from '../Types/PayloadSource' import { TransferPayload } from '../../TransferPayload/Interfaces/TransferPayload' import { ItemContent } from '../../Content/ItemContent' import { SyncResolvedParams, SyncResolvedPayload } from '../../../Runtime/Deltas/Utilities/SyncResolvedPayload' +import { PersistentSignatureData } from '../../../Runtime/Encryption/PersistentSignatureData' +import { ContentTypeUsesRootKeyEncryption } from '../../../Runtime/Encryption/ContentTypeUsesRootKeyEncryption' type RequiredKeepUndefined = { [K in keyof T]-?: [T[K]] } extends infer U ? U extends Record @@ -33,18 +35,28 @@ export abstract class PurePayload, C extends ItemCo readonly lastSyncEnd?: Date readonly duplicate_of?: string + readonly user_uuid?: string + readonly key_system_identifier?: string | undefined + readonly shared_vault_uuid?: string | undefined + readonly last_edited_by_uuid?: string + + readonly signatureData?: PersistentSignatureData constructor(rawPayload: T, source = PayloadSource.Constructor) { - this.source = source - this.uuid = rawPayload.uuid - - if (!this.uuid) { + if (!rawPayload.uuid) { throw Error( `Attempting to construct payload with null uuid Content type: ${rawPayload.content_type}`, ) } + if (rawPayload.key_system_identifier && ContentTypeUsesRootKeyEncryption(rawPayload.content_type)) { + throw new Error('Rootkey-encrypted payload should not have a key system identifier') + } + + this.source = source + this.uuid = rawPayload.uuid + this.content = rawPayload.content this.content_type = rawPayload.content_type this.deleted = useBoolean(rawPayload.deleted, false) @@ -63,6 +75,13 @@ export abstract class PurePayload, C extends ItemCo this.dirtyIndex = rawPayload.dirtyIndex this.globalDirtyIndexAtLastSync = rawPayload.globalDirtyIndexAtLastSync + this.user_uuid = rawPayload.user_uuid ?? undefined + this.key_system_identifier = rawPayload.key_system_identifier ?? undefined + this.shared_vault_uuid = rawPayload.shared_vault_uuid ?? undefined + this.last_edited_by_uuid = rawPayload.last_edited_by_uuid ?? undefined + + this.signatureData = rawPayload.signatureData + const timeToAllowSubclassesToFinishConstruction = 0 setTimeout(() => { deepFreeze(this) @@ -85,6 +104,11 @@ export abstract class PurePayload, C extends ItemCo globalDirtyIndexAtLastSync: this.globalDirtyIndexAtLastSync, lastSyncBegan: this.lastSyncBegan, lastSyncEnd: this.lastSyncEnd, + key_system_identifier: this.key_system_identifier, + user_uuid: this.user_uuid, + shared_vault_uuid: this.shared_vault_uuid, + last_edited_by_uuid: this.last_edited_by_uuid, + signatureData: this.signatureData, } return comprehensive diff --git a/packages/models/src/Domain/Abstract/Payload/Interfaces/PayloadInterface.ts b/packages/models/src/Domain/Abstract/Payload/Interfaces/PayloadInterface.ts index 42d65a0be..1819b11e6 100644 --- a/packages/models/src/Domain/Abstract/Payload/Interfaces/PayloadInterface.ts +++ b/packages/models/src/Domain/Abstract/Payload/Interfaces/PayloadInterface.ts @@ -3,6 +3,7 @@ import { ContentType } from '@standardnotes/common' import { ItemContent } from '../../Content/ItemContent' import { TransferPayload } from '../../TransferPayload/Interfaces/TransferPayload' import { PayloadSource } from '../Types/PayloadSource' +import { PersistentSignatureData } from '../../../Runtime/Encryption/PersistentSignatureData' export interface PayloadInterface { readonly source: PayloadSource @@ -22,12 +23,18 @@ export interface PayloadInterface { + if (!vault) { + return {} + } + + return { + key_system_identifier: vault.systemIdentifier, + shared_vault_uuid: vault.isSharedVaultListing() ? vault.sharing.sharedVaultUuid : undefined, + } +} diff --git a/packages/models/src/Domain/Abstract/Payload/Types/PayloadSource.ts b/packages/models/src/Domain/Abstract/Payload/Types/PayloadSource.ts index 4177073db..54bf455dd 100644 --- a/packages/models/src/Domain/Abstract/Payload/Types/PayloadSource.ts +++ b/packages/models/src/Domain/Abstract/Payload/Types/PayloadSource.ts @@ -5,6 +5,8 @@ export enum PayloadSource { */ Constructor = 1, + LocalDatabaseLoaded = 2, + RemoteRetrieved, RemoteSaved, diff --git a/packages/models/src/Domain/Abstract/Payload/index.ts b/packages/models/src/Domain/Abstract/Payload/index.ts index 50604221f..3baf73ad3 100644 --- a/packages/models/src/Domain/Abstract/Payload/index.ts +++ b/packages/models/src/Domain/Abstract/Payload/index.ts @@ -10,4 +10,5 @@ export * from './Interfaces/TypeCheck' export * from './Interfaces/UnionTypes' export * from './Types/PayloadSource' export * from './Types/EmitSource' -export * from './Types/TimestampDefaults' +export * from './Overrides/TimestampDefaults' +export * from './Overrides/VaultOverride' diff --git a/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TransferPayload.ts b/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TransferPayload.ts index b89ee43e1..91ec7be33 100644 --- a/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TransferPayload.ts +++ b/packages/models/src/Domain/Abstract/TransferPayload/Interfaces/TransferPayload.ts @@ -1,5 +1,6 @@ import { ContentType } from '@standardnotes/common' import { ItemContent } from '../../Content/ItemContent' +import { PersistentSignatureData } from '../../../Runtime/Encryption/PersistentSignatureData' export interface TransferPayload { uuid: string @@ -15,9 +16,16 @@ export interface TransferPayload { dirtyIndex?: number globalDirtyIndexAtLastSync?: number dirty?: boolean + signatureData?: PersistentSignatureData lastSyncBegan?: Date lastSyncEnd?: Date duplicate_of?: string + user_uuid?: string + + key_system_identifier?: string | undefined + shared_vault_uuid?: string | undefined + + last_edited_by_uuid?: string } diff --git a/packages/models/src/Domain/Local/KeyParams/KeySystemRootKeyParamsInterface.ts b/packages/models/src/Domain/Local/KeyParams/KeySystemRootKeyParamsInterface.ts new file mode 100644 index 000000000..709100c8d --- /dev/null +++ b/packages/models/src/Domain/Local/KeyParams/KeySystemRootKeyParamsInterface.ts @@ -0,0 +1,16 @@ +import { ProtocolVersion } from '@standardnotes/common' +import { KeySystemIdentifier } from '../../Syncable/KeySystemRootKey/KeySystemIdentifier' +import { KeySystemRootKeyPasswordType } from './KeySystemRootKeyPasswordType' + +/** + * Key params are public data that contain information about how a root key was created. + * Given a keyParams object and a password, clients can compute a root key that was created + * previously. + */ +export interface KeySystemRootKeyParamsInterface { + systemIdentifier: KeySystemIdentifier + seed: string + version: ProtocolVersion + passwordType: KeySystemRootKeyPasswordType + creationTimestamp: number +} diff --git a/packages/models/src/Domain/Local/KeyParams/KeySystemRootKeyPasswordType.ts b/packages/models/src/Domain/Local/KeyParams/KeySystemRootKeyPasswordType.ts new file mode 100644 index 000000000..8c5a117c7 --- /dev/null +++ b/packages/models/src/Domain/Local/KeyParams/KeySystemRootKeyPasswordType.ts @@ -0,0 +1,4 @@ +export enum KeySystemRootKeyPasswordType { + UserInputted = 'user_inputted', + Randomized = 'randomized', +} diff --git a/packages/models/src/Domain/Local/RootKey/KeychainTypes.ts b/packages/models/src/Domain/Local/RootKey/KeychainTypes.ts index 97cc09f40..b1be093a6 100644 --- a/packages/models/src/Domain/Local/RootKey/KeychainTypes.ts +++ b/packages/models/src/Domain/Local/RootKey/KeychainTypes.ts @@ -1,5 +1,6 @@ import { ApplicationIdentifier, ProtocolVersion } from '@standardnotes/common' import { RootKeyContentSpecialized } from './RootKeyContent' +import { PkcKeyPair } from '@standardnotes/sncrypto-common' export type RawKeychainValue = Record @@ -7,6 +8,8 @@ export interface NamespacedRootKeyInKeychain { version: ProtocolVersion masterKey: string dataAuthenticationKey?: string + encryptionKeyPair: PkcKeyPair | undefined + signingKeyPair: PkcKeyPair | undefined } export type RootKeyContentInStorage = RootKeyContentSpecialized diff --git a/packages/models/src/Domain/Local/RootKey/RootKeyContent.ts b/packages/models/src/Domain/Local/RootKey/RootKeyContent.ts index f4f1c56c7..fbbf1301b 100644 --- a/packages/models/src/Domain/Local/RootKey/RootKeyContent.ts +++ b/packages/models/src/Domain/Local/RootKey/RootKeyContent.ts @@ -1,3 +1,4 @@ +import { PkcKeyPair } from '@standardnotes/sncrypto-common' import { ItemContent } from '../../Abstract/Content/ItemContent' import { ProtocolVersion, AnyKeyParamsContent } from '@standardnotes/common' @@ -7,6 +8,9 @@ export interface RootKeyContentSpecialized { serverPassword?: string dataAuthenticationKey?: string keyParams: AnyKeyParamsContent + + encryptionKeyPair?: PkcKeyPair + signingKeyPair?: PkcKeyPair } export type RootKeyContent = RootKeyContentSpecialized & ItemContent diff --git a/packages/models/src/Domain/Local/RootKey/RootKeyInterface.ts b/packages/models/src/Domain/Local/RootKey/RootKeyInterface.ts index cae177525..58ab73b94 100644 --- a/packages/models/src/Domain/Local/RootKey/RootKeyInterface.ts +++ b/packages/models/src/Domain/Local/RootKey/RootKeyInterface.ts @@ -1,3 +1,4 @@ +import { PkcKeyPair } from '@standardnotes/sncrypto-common' import { ProtocolVersion } from '@standardnotes/common' import { DecryptedItemInterface } from '../../Abstract/Item/Interfaces/DecryptedItem' import { RootKeyParamsInterface } from '../KeyParams/RootKeyParamsInterface' @@ -6,11 +7,16 @@ import { RootKeyContent } from './RootKeyContent' export interface RootKeyInterface extends DecryptedItemInterface { readonly keyParams: RootKeyParamsInterface + get keyVersion(): ProtocolVersion get itemsKey(): string get masterKey(): string get serverPassword(): string | undefined get dataAuthenticationKey(): string | undefined + + get encryptionKeyPair(): PkcKeyPair | undefined + get signingKeyPair(): PkcKeyPair | undefined + compare(otherKey: RootKeyInterface): boolean persistableValueWhenWrapping(): RootKeyContentInStorage getKeychainValue(): NamespacedRootKeyInKeychain diff --git a/packages/models/src/Domain/Local/RootKey/RootKeyWithKeyPairsInterface.ts b/packages/models/src/Domain/Local/RootKey/RootKeyWithKeyPairsInterface.ts new file mode 100644 index 000000000..32392b108 --- /dev/null +++ b/packages/models/src/Domain/Local/RootKey/RootKeyWithKeyPairsInterface.ts @@ -0,0 +1,7 @@ +import { PkcKeyPair } from '@standardnotes/sncrypto-common' +import { RootKeyInterface } from './RootKeyInterface' + +export interface RootKeyWithKeyPairsInterface extends RootKeyInterface { + get encryptionKeyPair(): PkcKeyPair + get signingKeyPair(): PkcKeyPair +} diff --git a/packages/models/src/Domain/Runtime/AsymmetricMessage/AsymmetricMessageDataCommon.ts b/packages/models/src/Domain/Runtime/AsymmetricMessage/AsymmetricMessageDataCommon.ts new file mode 100644 index 000000000..1ce6c17b6 --- /dev/null +++ b/packages/models/src/Domain/Runtime/AsymmetricMessage/AsymmetricMessageDataCommon.ts @@ -0,0 +1,3 @@ +export type AsymmetricMessageDataCommon = { + recipientUuid: string +} diff --git a/packages/models/src/Domain/Runtime/AsymmetricMessage/AsymmetricMessagePayload.ts b/packages/models/src/Domain/Runtime/AsymmetricMessage/AsymmetricMessagePayload.ts new file mode 100644 index 000000000..6cecf24f3 --- /dev/null +++ b/packages/models/src/Domain/Runtime/AsymmetricMessage/AsymmetricMessagePayload.ts @@ -0,0 +1,12 @@ +import { AsymmetricMessageSenderKeypairChanged } from './MessageTypes/AsymmetricMessageSenderKeypairChanged' +import { AsymmetricMessageSharedVaultInvite } from './MessageTypes/AsymmetricMessageSharedVaultInvite' +import { AsymmetricMessageSharedVaultMetadataChanged } from './MessageTypes/AsymmetricMessageSharedVaultMetadataChanged' +import { AsymmetricMessageSharedVaultRootKeyChanged } from './MessageTypes/AsymmetricMessageSharedVaultRootKeyChanged' +import { AsymmetricMessageTrustedContactShare } from './MessageTypes/AsymmetricMessageTrustedContactShare' + +export type AsymmetricMessagePayload = + | AsymmetricMessageSharedVaultRootKeyChanged + | AsymmetricMessageTrustedContactShare + | AsymmetricMessageSenderKeypairChanged + | AsymmetricMessageSharedVaultInvite + | AsymmetricMessageSharedVaultMetadataChanged diff --git a/packages/models/src/Domain/Runtime/AsymmetricMessage/AsymmetricMessagePayloadType.ts b/packages/models/src/Domain/Runtime/AsymmetricMessage/AsymmetricMessagePayloadType.ts new file mode 100644 index 000000000..c4dab81c1 --- /dev/null +++ b/packages/models/src/Domain/Runtime/AsymmetricMessage/AsymmetricMessagePayloadType.ts @@ -0,0 +1,7 @@ +export enum AsymmetricMessagePayloadType { + ContactShare = 'contact-share', + SharedVaultRootKeyChanged = 'shared-vault-root-key-changed', + SenderKeypairChanged = 'sender-keypair-changed', + SharedVaultInvite = 'shared-vault-invite', + SharedVaultMetadataChanged = 'shared-vault-metadata-changed', +} diff --git a/packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSenderKeypairChanged.ts b/packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSenderKeypairChanged.ts new file mode 100644 index 000000000..254e4fb2a --- /dev/null +++ b/packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSenderKeypairChanged.ts @@ -0,0 +1,10 @@ +import { AsymmetricMessageDataCommon } from '../AsymmetricMessageDataCommon' +import { AsymmetricMessagePayloadType } from '../AsymmetricMessagePayloadType' + +export type AsymmetricMessageSenderKeypairChanged = { + type: AsymmetricMessagePayloadType.SenderKeypairChanged + data: AsymmetricMessageDataCommon & { + newEncryptionPublicKey: string + newSigningPublicKey: string + } +} diff --git a/packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultInvite.ts b/packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultInvite.ts new file mode 100644 index 000000000..0d798fddf --- /dev/null +++ b/packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultInvite.ts @@ -0,0 +1,16 @@ +import { KeySystemRootKeyContentSpecialized } from '../../../Syncable/KeySystemRootKey/KeySystemRootKeyContent' +import { TrustedContactContentSpecialized } from '../../../Syncable/TrustedContact/TrustedContactContent' +import { AsymmetricMessageDataCommon } from '../AsymmetricMessageDataCommon' +import { AsymmetricMessagePayloadType } from '../AsymmetricMessagePayloadType' + +export type AsymmetricMessageSharedVaultInvite = { + type: AsymmetricMessagePayloadType.SharedVaultInvite + data: AsymmetricMessageDataCommon & { + rootKey: KeySystemRootKeyContentSpecialized + trustedContacts: TrustedContactContentSpecialized[] + metadata: { + name: string + description?: string + } + } +} diff --git a/packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultMetadataChanged.ts b/packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultMetadataChanged.ts new file mode 100644 index 000000000..6b3e308a5 --- /dev/null +++ b/packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultMetadataChanged.ts @@ -0,0 +1,11 @@ +import { AsymmetricMessageDataCommon } from '../AsymmetricMessageDataCommon' +import { AsymmetricMessagePayloadType } from '../AsymmetricMessagePayloadType' + +export type AsymmetricMessageSharedVaultMetadataChanged = { + type: AsymmetricMessagePayloadType.SharedVaultMetadataChanged + data: AsymmetricMessageDataCommon & { + sharedVaultUuid: string + name: string + description?: string + } +} diff --git a/packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultRootKeyChanged.ts b/packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultRootKeyChanged.ts new file mode 100644 index 000000000..c65c6a578 --- /dev/null +++ b/packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultRootKeyChanged.ts @@ -0,0 +1,8 @@ +import { KeySystemRootKeyContentSpecialized } from '../../../Syncable/KeySystemRootKey/KeySystemRootKeyContent' +import { AsymmetricMessageDataCommon } from '../AsymmetricMessageDataCommon' +import { AsymmetricMessagePayloadType } from '../AsymmetricMessagePayloadType' + +export type AsymmetricMessageSharedVaultRootKeyChanged = { + type: AsymmetricMessagePayloadType.SharedVaultRootKeyChanged + data: AsymmetricMessageDataCommon & { rootKey: KeySystemRootKeyContentSpecialized } +} diff --git a/packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageTrustedContactShare.ts b/packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageTrustedContactShare.ts new file mode 100644 index 000000000..9196df598 --- /dev/null +++ b/packages/models/src/Domain/Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageTrustedContactShare.ts @@ -0,0 +1,8 @@ +import { TrustedContactContentSpecialized } from '../../../Syncable/TrustedContact/TrustedContactContent' +import { AsymmetricMessageDataCommon } from '../AsymmetricMessageDataCommon' +import { AsymmetricMessagePayloadType } from '../AsymmetricMessagePayloadType' + +export type AsymmetricMessageTrustedContactShare = { + type: AsymmetricMessagePayloadType.ContactShare + data: AsymmetricMessageDataCommon & { trustedContact: TrustedContactContentSpecialized } +} diff --git a/packages/models/src/Domain/Runtime/Collection/Item/TagItemsIndex.spec.ts b/packages/models/src/Domain/Runtime/Collection/Item/ItemCounter.spec.ts similarity index 94% rename from packages/models/src/Domain/Runtime/Collection/Item/TagItemsIndex.spec.ts rename to packages/models/src/Domain/Runtime/Collection/Item/ItemCounter.spec.ts index 0ffbfb733..ba9dd2ec4 100644 --- a/packages/models/src/Domain/Runtime/Collection/Item/TagItemsIndex.spec.ts +++ b/packages/models/src/Domain/Runtime/Collection/Item/ItemCounter.spec.ts @@ -1,10 +1,10 @@ +import { ItemCounter } from './ItemCounter' import { NoteContent } from '../../../Syncable/Note/NoteContent' import { ContentType } from '@standardnotes/common' import { DecryptedItem, EncryptedItem } from '../../../Abstract/Item' import { DecryptedPayload, EncryptedPayload, PayloadTimestampDefaults } from '../../../Abstract/Payload' import { ItemCollection } from './ItemCollection' import { FillItemContent } from '../../../Abstract/Content/ItemContent' -import { TagItemsIndex } from './TagItemsIndex' import { ItemDelta } from '../../Index/ItemDelta' import { AnyItemInterface } from '../../../Abstract/Item/Interfaces/UnionTypes' @@ -48,7 +48,7 @@ describe('tag notes index', () => { it('should count both notes and files', () => { const collection = new ItemCollection() - const index = new TagItemsIndex(collection) + const index = new ItemCounter(collection) const decryptedNote = createDecryptedItem('note') const decryptedFile = createDecryptedItem('file') @@ -61,7 +61,7 @@ describe('tag notes index', () => { it('should decrement count after decrypted note becomes errored', () => { const collection = new ItemCollection() - const index = new TagItemsIndex(collection) + const index = new ItemCounter(collection) const decryptedItem = createDecryptedItem() collection.set(decryptedItem) diff --git a/packages/models/src/Domain/Runtime/Collection/Item/TagItemsIndex.ts b/packages/models/src/Domain/Runtime/Collection/Item/ItemCounter.ts similarity index 61% rename from packages/models/src/Domain/Runtime/Collection/Item/TagItemsIndex.ts rename to packages/models/src/Domain/Runtime/Collection/Item/ItemCounter.ts index 0dac76acf..869d05e45 100644 --- a/packages/models/src/Domain/Runtime/Collection/Item/TagItemsIndex.ts +++ b/packages/models/src/Domain/Runtime/Collection/Item/ItemCounter.ts @@ -4,25 +4,28 @@ import { isTag, SNTag } from '../../../Syncable/Tag/Tag' import { SNIndex } from '../../Index/SNIndex' import { ItemCollection } from './ItemCollection' import { ItemDelta } from '../../Index/ItemDelta' -import { isDecryptedItem, ItemInterface } from '../../../Abstract/Item' +import { DecryptedItemInterface, isDecryptedItem, ItemInterface } from '../../../Abstract/Item' +import { CriteriaValidatorInterface } from '../../Display/Validator/CriteriaValidatorInterface' +import { CollectionCriteriaValidator } from '../../Display/Validator/CollectionCriteriaValidator' +import { ExcludeVaultsCriteriaValidator } from '../../Display/Validator/ExcludeVaultsCriteriaValidator' +import { ExclusiveVaultCriteriaValidator } from '../../Display/Validator/ExclusiveVaultCriteriaValidator' +import { HiddenContentCriteriaValidator } from '../../Display/Validator/HiddenContentCriteriaValidator' +import { CustomFilterCriteriaValidator } from '../../Display/Validator/CustomFilterCriteriaValidator' +import { AnyDisplayOptions, VaultDisplayOptions } from '../../Display' +import { isExclusioanaryOptionsValue } from '../../Display/VaultDisplayOptionsTypes' type AllNotesUuidSignifier = undefined export type TagItemCountChangeObserver = (tagUuid: string | AllNotesUuidSignifier) => void -export class TagItemsIndex implements SNIndex { +export class ItemCounter implements SNIndex { private tagToItemsMap: Partial>> = {} private allCountableItems = new Set() private countableItemsByType = new Map>() + private displayOptions?: AnyDisplayOptions + private vaultDisplayOptions?: VaultDisplayOptions constructor(private collection: ItemCollection, public observers: TagItemCountChangeObserver[] = []) {} - private isItemCountable = (item: ItemInterface) => { - if (isDecryptedItem(item)) { - return !item.archived && !item.trashed && !item.conflictOf - } - return false - } - public addCountChangeObserver(observer: TagItemCountChangeObserver): () => void { this.observers.push(observer) @@ -32,10 +35,14 @@ export class TagItemsIndex implements SNIndex { } } - private notifyObservers(tagUuid: string | undefined) { - for (const observer of this.observers) { - observer(tagUuid) - } + public setDisplayOptions(options: AnyDisplayOptions) { + this.displayOptions = options + this.receiveItemChanges(this.collection.all()) + } + + public setVaultDisplayOptions(options: VaultDisplayOptions) { + this.vaultDisplayOptions = options + this.receiveItemChanges(this.collection.all()) } public allCountableItemsCount(): number { @@ -64,6 +71,47 @@ export class TagItemsIndex implements SNIndex { this.receiveTagChanges(tags) } + private passesAllFilters(element: DecryptedItemInterface): boolean { + if (!this.displayOptions) { + return true + } + + const filters: CriteriaValidatorInterface[] = [new CollectionCriteriaValidator(this.collection, element)] + + if (this.vaultDisplayOptions) { + const options = this.vaultDisplayOptions.getOptions() + if (isExclusioanaryOptionsValue(options)) { + filters.push(new ExcludeVaultsCriteriaValidator([...options.exclude, ...options.locked], element)) + } else { + filters.push(new ExclusiveVaultCriteriaValidator(options.exclusive, element)) + } + } + + if ('hiddenContentTypes' in this.displayOptions && this.displayOptions.hiddenContentTypes) { + filters.push(new HiddenContentCriteriaValidator(this.displayOptions.hiddenContentTypes, element)) + } + + if ('customFilter' in this.displayOptions && this.displayOptions.customFilter) { + filters.push(new CustomFilterCriteriaValidator(this.displayOptions.customFilter, element)) + } + + return filters.every((f) => f.passes()) + } + + private isItemCountable = (item: ItemInterface) => { + if (isDecryptedItem(item)) { + const passesFilters = this.passesAllFilters(item) + return passesFilters && !item.archived && !item.trashed && !item.conflictOf + } + return false + } + + private notifyObservers(tagUuid: string | undefined) { + for (const observer of this.observers) { + observer(tagUuid) + } + } + private receiveTagChanges(tags: SNTag[]): void { for (const tag of tags) { const uuids = tag.references diff --git a/packages/models/src/Domain/Runtime/Deltas/RemoteDataConflicts.ts b/packages/models/src/Domain/Runtime/Deltas/RemoteDataConflicts.ts index e78d27b6d..cf1ce2e21 100644 --- a/packages/models/src/Domain/Runtime/Deltas/RemoteDataConflicts.ts +++ b/packages/models/src/Domain/Runtime/Deltas/RemoteDataConflicts.ts @@ -1,15 +1,16 @@ import { ConflictDelta } from './Conflict' -import { PayloadEmitSource } from '../../Abstract/Payload' +import { FullyFormedPayloadInterface, PayloadEmitSource } from '../../Abstract/Payload' import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection' import { HistoryMap } from '../History' import { extendSyncDelta, SyncDeltaEmit } from './Abstract/DeltaEmit' import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface' import { payloadByFinalizingSyncState } from './Utilities/ApplyDirtyState' +import { ConflictConflictingDataParams } from '@standardnotes/responses' export class DeltaRemoteDataConflicts implements SyncDeltaInterface { constructor( readonly baseCollection: ImmutablePayloadCollection, - readonly applyCollection: ImmutablePayloadCollection, + readonly conflicts: ConflictConflictingDataParams[], readonly historyMap: HistoryMap, ) {} @@ -20,18 +21,18 @@ export class DeltaRemoteDataConflicts implements SyncDeltaInterface { source: PayloadEmitSource.RemoteRetrieved, } - for (const apply of this.applyCollection.all()) { - const base = this.baseCollection.find(apply.uuid) + for (const conflict of this.conflicts) { + const base = this.baseCollection.find(conflict.server_item.uuid) const isBaseDeleted = base == undefined if (isBaseDeleted) { - result.emits.push(payloadByFinalizingSyncState(apply, this.baseCollection)) + result.emits.push(payloadByFinalizingSyncState(conflict.server_item, this.baseCollection)) continue } - const delta = new ConflictDelta(this.baseCollection, base, apply, this.historyMap) + const delta = new ConflictDelta(this.baseCollection, base, conflict.server_item, this.historyMap) extendSyncDelta(result, delta.result()) } diff --git a/packages/models/src/Domain/Runtime/Deltas/RemoteRejected.spec.ts b/packages/models/src/Domain/Runtime/Deltas/RemoteRejected.spec.ts index d695d90de..17304f5d9 100644 --- a/packages/models/src/Domain/Runtime/Deltas/RemoteRejected.spec.ts +++ b/packages/models/src/Domain/Runtime/Deltas/RemoteRejected.spec.ts @@ -1,10 +1,14 @@ import { ContentType } from '@standardnotes/common' import { FillItemContent } from '../../Abstract/Content/ItemContent' -import { DecryptedPayload, PayloadTimestampDefaults } from '../../Abstract/Payload' +import { DecryptedPayload, FullyFormedPayloadInterface, PayloadTimestampDefaults } from '../../Abstract/Payload' import { NoteContent } from '../../Syncable/Note' import { PayloadCollection } from '../Collection/Payload/PayloadCollection' import { DeltaRemoteRejected } from './RemoteRejected' import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection' +import { ConflictParams, ConflictType } from '@standardnotes/responses' +import { UuidGenerator } from '@standardnotes/utils' + +UuidGenerator.SetGenerator(() => String(Math.random())) describe('remote rejected delta', () => { it('rejected payloads should not map onto app state', async () => { @@ -30,10 +34,12 @@ describe('remote rejected delta', () => { dirty: true, }) - const delta = new DeltaRemoteRejected( - ImmutablePayloadCollection.FromCollection(baseCollection), - ImmutablePayloadCollection.WithPayloads([rejectedPayload]), - ) + const entry: ConflictParams = { + type: ConflictType.ContentTypeError, + unsaved_item: rejectedPayload, + } as unknown as ConflictParams + + const delta = new DeltaRemoteRejected(ImmutablePayloadCollection.FromCollection(baseCollection), [entry]) const result = delta.result() const payload = result.emits[0] as DecryptedPayload diff --git a/packages/models/src/Domain/Runtime/Deltas/RemoteRejected.ts b/packages/models/src/Domain/Runtime/Deltas/RemoteRejected.ts index f52f8dc86..aac65c260 100644 --- a/packages/models/src/Domain/Runtime/Deltas/RemoteRejected.ts +++ b/packages/models/src/Domain/Runtime/Deltas/RemoteRejected.ts @@ -1,35 +1,48 @@ import { PayloadSource } from '../../Abstract/Payload/Types/PayloadSource' -import { PayloadEmitSource } from '../../Abstract/Payload' +import { DeletedPayload, FullyFormedPayloadInterface, PayloadEmitSource } from '../../Abstract/Payload' import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection' import { SyncDeltaEmit } from './Abstract/DeltaEmit' import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface' -import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload' +import { BuildSyncResolvedParams, SyncResolvedPayload } from './Utilities/SyncResolvedPayload' +import { + ConflictParams, + ConflictParamsWithServerItem, + ConflictParamsWithUnsavedItem, + ConflictParamsWithServerAndUnsavedItem, + conflictParamsHasServerItemAndUnsavedItem, + conflictParamsHasOnlyServerItem, + conflictParamsHasOnlyUnsavedItem, + ConflictType, +} from '@standardnotes/responses' +import { PayloadsByDuplicating } from '../../Utilities/Payload/PayloadsByDuplicating' +import { ContentType } from '@standardnotes/common' export class DeltaRemoteRejected implements SyncDeltaInterface { constructor( readonly baseCollection: ImmutablePayloadCollection, - readonly applyCollection: ImmutablePayloadCollection, + readonly conflicts: ConflictParams[], ) {} public result(): SyncDeltaEmit { const results: SyncResolvedPayload[] = [] - for (const apply of this.applyCollection.all()) { - const base = this.baseCollection.find(apply.uuid) + const vaultErrors: ConflictType[] = [ + ConflictType.SharedVaultInsufficientPermissionsError, + ConflictType.SharedVaultNotMemberError, + ConflictType.SharedVaultInvalidState, + ConflictType.SharedVaultSnjsVersionError, + ] - if (!base) { - continue + for (const conflict of this.conflicts) { + if (vaultErrors.includes(conflict.type)) { + results.push(...this.handleVaultError(conflict)) + } else if (conflictParamsHasServerItemAndUnsavedItem(conflict)) { + results.push(...this.getResultForConflictWithServerItemAndUnsavedItem(conflict)) + } else if (conflictParamsHasOnlyServerItem(conflict)) { + results.push(...this.getResultForConflictWithOnlyServerItem(conflict)) + } else if (conflictParamsHasOnlyUnsavedItem(conflict)) { + results.push(...this.getResultForConflictWithOnlyUnsavedItem(conflict)) } - - const result = base.copyAsSyncResolved( - { - dirty: false, - lastSyncEnd: new Date(), - }, - PayloadSource.RemoteSaved, - ) - - results.push(result) } return { @@ -37,4 +50,177 @@ export class DeltaRemoteRejected implements SyncDeltaInterface { source: PayloadEmitSource.RemoteSaved, } } + + private handleVaultError(conflict: ConflictParams): SyncResolvedPayload[] { + const base = this.baseCollection.find(conflict.unsaved_item.uuid) + if (!base) { + return [] + } + + if (conflict.type === ConflictType.SharedVaultNotMemberError) { + return this.resultByDuplicatingBasePayloadAsNonVaultedAndRemovingBaseItemLocally(base) + } + + if (base.content_type === ContentType.KeySystemItemsKey) { + return this.discardChangesOfBasePayload(base) + } + + if (conflict.server_item) { + return this.resultByDuplicatingBasePayloadAsNonVaultedAndTakingServerPayloadAsCanonical( + base, + conflict.server_item, + ) + } else { + return this.resultByDuplicatingBasePayloadAsNonVaultedAndDiscardingChangesOfOriginal(base) + } + } + + private discardChangesOfBasePayload(base: FullyFormedPayloadInterface): SyncResolvedPayload[] { + const undirtiedPayload = base.copyAsSyncResolved( + { + dirty: false, + lastSyncEnd: new Date(), + }, + PayloadSource.RemoteSaved, + ) + + return [undirtiedPayload] + } + + private getResultForConflictWithOnlyUnsavedItem( + conflict: ConflictParamsWithUnsavedItem, + ): SyncResolvedPayload[] { + const base = this.baseCollection.find(conflict.unsaved_item.uuid) + if (!base) { + return [] + } + + const result = base.copyAsSyncResolved( + { + dirty: false, + lastSyncEnd: new Date(), + }, + PayloadSource.RemoteSaved, + ) + + return [result] + } + + private getResultForConflictWithOnlyServerItem( + conflict: ConflictParamsWithServerItem, + ): SyncResolvedPayload[] { + const base = this.baseCollection.find(conflict.server_item.uuid) + if (!base) { + return [] + } + + return this.resultByDuplicatingBasePayloadIntoNewUuidAndTakingServerPayloadAsCanonical(base, conflict.server_item) + } + + private getResultForConflictWithServerItemAndUnsavedItem( + conflict: ConflictParamsWithServerAndUnsavedItem, + ): SyncResolvedPayload[] { + const base = this.baseCollection.find(conflict.server_item.uuid) + if (!base) { + return [] + } + + return this.resultByDuplicatingBasePayloadIntoNewUuidAndTakingServerPayloadAsCanonical(base, conflict.server_item) + } + + private resultByDuplicatingBasePayloadIntoNewUuidAndTakingServerPayloadAsCanonical( + basePayload: FullyFormedPayloadInterface, + serverPayload: FullyFormedPayloadInterface, + ): SyncResolvedPayload[] { + const duplicateBasePayloadIntoNewUuid = PayloadsByDuplicating({ + payload: basePayload, + baseCollection: this.baseCollection, + isConflict: true, + source: serverPayload.source, + }) + + const takeServerPayloadAsCanonical = serverPayload.copyAsSyncResolved( + { + lastSyncBegan: basePayload.lastSyncBegan, + dirty: false, + lastSyncEnd: new Date(), + }, + serverPayload.source, + ) + + return duplicateBasePayloadIntoNewUuid.concat([takeServerPayloadAsCanonical]) + } + + private resultByDuplicatingBasePayloadAsNonVaultedAndTakingServerPayloadAsCanonical( + basePayload: FullyFormedPayloadInterface, + serverPayload: FullyFormedPayloadInterface, + ): SyncResolvedPayload[] { + const duplicateBasePayloadIntoNewUuid = PayloadsByDuplicating({ + payload: basePayload.copy({ + key_system_identifier: undefined, + shared_vault_uuid: undefined, + }), + baseCollection: this.baseCollection, + isConflict: true, + source: serverPayload.source, + }) + + const takeServerPayloadAsCanonical = serverPayload.copyAsSyncResolved( + { + lastSyncBegan: basePayload.lastSyncBegan, + dirty: false, + lastSyncEnd: new Date(), + }, + serverPayload.source, + ) + + return duplicateBasePayloadIntoNewUuid.concat([takeServerPayloadAsCanonical]) + } + + private resultByDuplicatingBasePayloadAsNonVaultedAndDiscardingChangesOfOriginal( + basePayload: FullyFormedPayloadInterface, + ): SyncResolvedPayload[] { + const duplicateBasePayloadWithoutVault = PayloadsByDuplicating({ + payload: basePayload.copy({ + key_system_identifier: undefined, + shared_vault_uuid: undefined, + }), + baseCollection: this.baseCollection, + isConflict: true, + source: basePayload.source, + }) + + return [...duplicateBasePayloadWithoutVault, ...this.discardChangesOfBasePayload(basePayload)] + } + + private resultByDuplicatingBasePayloadAsNonVaultedAndRemovingBaseItemLocally( + basePayload: FullyFormedPayloadInterface, + ): SyncResolvedPayload[] { + const duplicateBasePayloadWithoutVault = PayloadsByDuplicating({ + payload: basePayload.copy({ + key_system_identifier: undefined, + shared_vault_uuid: undefined, + }), + baseCollection: this.baseCollection, + isConflict: true, + source: basePayload.source, + }) + + const locallyDeletedBasePayload = new DeletedPayload( + { + ...basePayload, + content: undefined, + deleted: true, + key_system_identifier: undefined, + shared_vault_uuid: undefined, + ...BuildSyncResolvedParams({ + dirty: false, + lastSyncEnd: new Date(), + }), + }, + PayloadSource.RemoteSaved, + ) + + return [...duplicateBasePayloadWithoutVault, locallyDeletedBasePayload as SyncResolvedPayload] + } } diff --git a/packages/models/src/Domain/Runtime/Deltas/RemoteRetrieved.ts b/packages/models/src/Domain/Runtime/Deltas/RemoteRetrieved.ts index 273b15cfc..d441cba8b 100644 --- a/packages/models/src/Domain/Runtime/Deltas/RemoteRetrieved.ts +++ b/packages/models/src/Domain/Runtime/Deltas/RemoteRetrieved.ts @@ -36,7 +36,7 @@ export class DeltaRemoteRetrieved implements SyncDeltaInterface { * or if the item is locally dirty, filter it out of retrieved_items, and add to potential conflicts. */ for (const apply of this.applyCollection.all()) { - if (apply.content_type === ContentType.ItemsKey) { + if (apply.content_type === ContentType.ItemsKey || apply.content_type === ContentType.KeySystemItemsKey) { const itemsKeyDeltaEmit = new ItemsKeyDelta(this.baseCollection, [apply]).result() extendSyncDelta(result, itemsKeyDeltaEmit) diff --git a/packages/models/src/Domain/Runtime/Deltas/RemoteSaved.ts b/packages/models/src/Domain/Runtime/Deltas/RemoteSaved.ts index f3d74a945..af8af892d 100644 --- a/packages/models/src/Domain/Runtime/Deltas/RemoteSaved.ts +++ b/packages/models/src/Domain/Runtime/Deltas/RemoteSaved.ts @@ -1,4 +1,4 @@ -import { ServerSyncSavedContextualPayload } from './../../Abstract/Contextual/ServerSyncSaved' +import { ServerSyncSavedContextualPayload } from '../../Abstract/Contextual/ServerSyncSaved' import { DeletedPayload } from './../../Abstract/Payload/Implementations/DeletedPayload' import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection' import { PayloadSource } from '../../Abstract/Payload/Types/PayloadSource' diff --git a/packages/models/src/Domain/Runtime/Deltas/RemoteUuidConflicts.ts b/packages/models/src/Domain/Runtime/Deltas/RemoteUuidConflicts.ts index a7d484b2e..5706b1761 100644 --- a/packages/models/src/Domain/Runtime/Deltas/RemoteUuidConflicts.ts +++ b/packages/models/src/Domain/Runtime/Deltas/RemoteUuidConflicts.ts @@ -2,10 +2,11 @@ import { extendArray, filterFromArray, Uuids } from '@standardnotes/utils' import { ImmutablePayloadCollection } from '../Collection/Payload/ImmutablePayloadCollection' import { PayloadsByAlternatingUuid } from '../../Utilities/Payload/PayloadsByAlternatingUuid' import { isDecryptedPayload } from '../../Abstract/Payload/Interfaces/TypeCheck' -import { PayloadEmitSource } from '../../Abstract/Payload' +import { FullyFormedPayloadInterface, PayloadEmitSource } from '../../Abstract/Payload' import { SyncDeltaEmit } from './Abstract/DeltaEmit' import { SyncDeltaInterface } from './Abstract/SyncDeltaInterface' import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload' +import { ConflictUuidConflictParams } from '@standardnotes/responses' /** * UUID conflicts can occur if a user attmpts to import an old data @@ -15,22 +16,22 @@ import { SyncResolvedPayload } from './Utilities/SyncResolvedPayload' export class DeltaRemoteUuidConflicts implements SyncDeltaInterface { constructor( readonly baseCollection: ImmutablePayloadCollection, - readonly applyCollection: ImmutablePayloadCollection, + readonly conflicts: ConflictUuidConflictParams[], ) {} public result(): SyncDeltaEmit { const results: SyncResolvedPayload[] = [] const baseCollectionCopy = this.baseCollection.mutableCopy() - for (const apply of this.applyCollection.all()) { + for (const conflict of this.conflicts) { /** * The payload in question may have been modified as part of alternating a uuid for * another item. For example, alternating a uuid for a note will also affect the * referencing tag, which would be added to `results`, but could also be inside * of this.applyCollection. In this case we'd prefer the most recently modified value. */ - const moreRecent = results.find((r) => r.uuid === apply.uuid) - const useApply = moreRecent || apply + const moreRecent = results.find((r) => r.uuid === conflict.unsaved_item.uuid) + const useApply = moreRecent || conflict.unsaved_item if (!isDecryptedPayload(useApply)) { continue diff --git a/packages/models/src/Domain/Runtime/Display/DisplayOptions.spec.ts b/packages/models/src/Domain/Runtime/Display/DisplayOptions.spec.ts index 0ed818617..2b6d79e11 100644 --- a/packages/models/src/Domain/Runtime/Display/DisplayOptions.spec.ts +++ b/packages/models/src/Domain/Runtime/Display/DisplayOptions.spec.ts @@ -1,8 +1,8 @@ import { createNoteWithContent } from '../../Utilities/Test/SpecUtils' import { ItemCollection } from '../Collection/Item/ItemCollection' import { SNNote } from '../../Syncable/Note/Note' -import { itemsMatchingOptions } from './Search/SearchUtilities' -import { FilterDisplayOptions } from './DisplayOptions' +import { notesAndFilesMatchingOptions } from './Search/SearchUtilities' +import { NotesAndFilesDisplayOptions } from './DisplayOptions' describe('item display options', () => { const collectionWithNotes = function (titles: (string | undefined)[] = [], bodies: string[] = []) { @@ -23,31 +23,31 @@ describe('item display options', () => { it('string query title', () => { const query = 'foo' - const options: FilterDisplayOptions = { + const options: NotesAndFilesDisplayOptions = { searchQuery: { query: query, includeProtectedNoteText: true }, - } + } as jest.Mocked const collection = collectionWithNotes(['hello', 'fobar', 'foobar', 'foo']) - expect(itemsMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2) + expect(notesAndFilesMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2) }) it('string query text', async function () { const query = 'foo' - const options: FilterDisplayOptions = { + const options: NotesAndFilesDisplayOptions = { searchQuery: { query: query, includeProtectedNoteText: true }, - } + } as jest.Mocked const collection = collectionWithNotes( [undefined, undefined, undefined, undefined], ['hello', 'fobar', 'foobar', 'foo'], ) - expect(itemsMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2) + expect(notesAndFilesMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2) }) it('string query title and text', async function () { const query = 'foo' - const options: FilterDisplayOptions = { + const options: NotesAndFilesDisplayOptions = { searchQuery: { query: query, includeProtectedNoteText: true }, - } + } as jest.Mocked const collection = collectionWithNotes(['hello', 'foobar'], ['foo', 'fobar']) - expect(itemsMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2) + expect(notesAndFilesMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2) }) }) diff --git a/packages/models/src/Domain/Runtime/Display/DisplayOptions.ts b/packages/models/src/Domain/Runtime/Display/DisplayOptions.ts index 64a88771f..25a215398 100644 --- a/packages/models/src/Domain/Runtime/Display/DisplayOptions.ts +++ b/packages/models/src/Domain/Runtime/Display/DisplayOptions.ts @@ -5,21 +5,28 @@ import { CollectionSortDirection, CollectionSortProperty } from '../Collection/C import { SearchQuery } from './Search/Types' import { DisplayControllerCustomFilter } from './Types' -export type DisplayOptions = FilterDisplayOptions & DisplayControllerOptions - -export interface FilterDisplayOptions { - tags?: SNTag[] - views?: SmartView[] - searchQuery?: SearchQuery +export interface GenericDisplayOptions { includePinned?: boolean includeProtected?: boolean includeTrashed?: boolean includeArchived?: boolean } -export interface DisplayControllerOptions { - sortBy: CollectionSortProperty - sortDirection: CollectionSortDirection +export interface NotesAndFilesDisplayOptions extends GenericDisplayOptions { + tags?: SNTag[] + views?: SmartView[] + searchQuery?: SearchQuery hiddenContentTypes?: ContentType[] customFilter?: DisplayControllerCustomFilter } + +export type TagsDisplayOptions = GenericDisplayOptions + +export interface DisplayControllerDisplayOptions extends GenericDisplayOptions { + sortBy: CollectionSortProperty + sortDirection: CollectionSortDirection +} + +export type NotesAndFilesDisplayControllerOptions = NotesAndFilesDisplayOptions & DisplayControllerDisplayOptions +export type TagsDisplayControllerOptions = TagsDisplayOptions & DisplayControllerDisplayOptions +export type AnyDisplayOptions = NotesAndFilesDisplayOptions | TagsDisplayOptions | GenericDisplayOptions diff --git a/packages/models/src/Domain/Runtime/Display/DisplayOptionsToFilters.ts b/packages/models/src/Domain/Runtime/Display/DisplayOptionsToFilters.ts index 90a029d45..4c51e32dc 100644 --- a/packages/models/src/Domain/Runtime/Display/DisplayOptionsToFilters.ts +++ b/packages/models/src/Domain/Runtime/Display/DisplayOptionsToFilters.ts @@ -5,11 +5,11 @@ import { CompoundPredicate } from '../Predicate/CompoundPredicate' import { ItemWithTags } from './Search/ItemWithTags' import { itemMatchesQuery, itemPassesFilters } from './Search/SearchUtilities' import { ItemFilter, ReferenceLookupCollection, SearchableDecryptedItem } from './Search/Types' -import { FilterDisplayOptions } from './DisplayOptions' +import { NotesAndFilesDisplayOptions } from './DisplayOptions' import { SystemViewId } from '../../Syncable/SmartView' export function computeUnifiedFilterForDisplayOptions( - options: FilterDisplayOptions, + options: NotesAndFilesDisplayOptions, collection: ReferenceLookupCollection, additionalFilters: ItemFilter[] = [], ): ItemFilter { @@ -21,7 +21,7 @@ export function computeUnifiedFilterForDisplayOptions( } export function computeFiltersForDisplayOptions( - options: FilterDisplayOptions, + options: NotesAndFilesDisplayOptions, collection: ReferenceLookupCollection, ): ItemFilter[] { const filters: ItemFilter[] = [] diff --git a/packages/models/src/Domain/Runtime/Display/ItemDisplayController.ts b/packages/models/src/Domain/Runtime/Display/ItemDisplayController.ts index 60c35fe13..f438860e4 100644 --- a/packages/models/src/Domain/Runtime/Display/ItemDisplayController.ts +++ b/packages/models/src/Domain/Runtime/Display/ItemDisplayController.ts @@ -2,11 +2,19 @@ import { ContentType } from '@standardnotes/common' import { compareValues } from '@standardnotes/utils' import { isDeletedItem, isEncryptedItem } from '../../Abstract/Item' import { ItemDelta } from '../Index/ItemDelta' -import { DisplayControllerOptions } from './DisplayOptions' +import { AnyDisplayOptions, DisplayControllerDisplayOptions, GenericDisplayOptions } from './DisplayOptions' import { sortTwoItems } from './SortTwoItems' import { UuidToSortedPositionMap, DisplayItem, ReadonlyItemCollection } from './Types' +import { CriteriaValidatorInterface } from './Validator/CriteriaValidatorInterface' +import { CollectionCriteriaValidator } from './Validator/CollectionCriteriaValidator' +import { CustomFilterCriteriaValidator } from './Validator/CustomFilterCriteriaValidator' +import { ExcludeVaultsCriteriaValidator } from './Validator/ExcludeVaultsCriteriaValidator' +import { ExclusiveVaultCriteriaValidator } from './Validator/ExclusiveVaultCriteriaValidator' +import { HiddenContentCriteriaValidator } from './Validator/HiddenContentCriteriaValidator' +import { VaultDisplayOptions } from './VaultDisplayOptions' +import { isExclusioanaryOptionsValue } from './VaultDisplayOptionsTypes' -export class ItemDisplayController { +export class ItemDisplayController { private sortMap: UuidToSortedPositionMap = {} private sortedItems: I[] = [] private needsSort = true @@ -14,7 +22,8 @@ export class ItemDisplayController { constructor( private readonly collection: ReadonlyItemCollection, public readonly contentTypes: ContentType[], - private options: DisplayControllerOptions, + private options: DisplayControllerDisplayOptions & O, + private vaultOptions?: VaultDisplayOptions, ) { this.filterThenSortElements(this.collection.all(this.contentTypes) as I[]) } @@ -23,7 +32,18 @@ export class ItemDisplayController { return this.sortedItems } - setDisplayOptions(displayOptions: Partial): void { + public getDisplayOptions(): DisplayControllerDisplayOptions & O { + return this.options + } + + setVaultDisplayOptions(vaultOptions?: VaultDisplayOptions): void { + this.vaultOptions = vaultOptions + this.needsSort = true + + this.filterThenSortElements(this.collection.all(this.contentTypes) as I[]) + } + + setDisplayOptions(displayOptions: Partial): void { this.options = { ...this.options, ...displayOptions } this.needsSort = true @@ -37,6 +57,29 @@ export class ItemDisplayController { this.filterThenSortElements(items as I[]) } + private passesAllFilters(element: I): boolean { + const filters: CriteriaValidatorInterface[] = [new CollectionCriteriaValidator(this.collection, element)] + + if (this.vaultOptions) { + const options = this.vaultOptions.getOptions() + if (isExclusioanaryOptionsValue(options)) { + filters.push(new ExcludeVaultsCriteriaValidator([...options.exclude, ...options.locked], element)) + } else { + filters.push(new ExclusiveVaultCriteriaValidator(options.exclusive, element)) + } + } + + if ('hiddenContentTypes' in this.options && this.options.hiddenContentTypes) { + filters.push(new HiddenContentCriteriaValidator(this.options.hiddenContentTypes, element)) + } + + if ('customFilter' in this.options && this.options.customFilter) { + filters.push(new CustomFilterCriteriaValidator(this.options.customFilter, element)) + } + + return filters.every((f) => f.passes()) + } + private filterThenSortElements(elements: I[]): void { for (const element of elements) { const previousIndex = this.sortMap[element.uuid] @@ -61,13 +104,7 @@ export class ItemDisplayController { continue } - const passes = !this.collection.has(element.uuid) - ? false - : this.options.hiddenContentTypes?.includes(element.content_type) - ? false - : this.options.customFilter - ? this.options.customFilter(element) - : true + const passes = this.passesAllFilters(element) if (passes) { if (previousElement != undefined) { diff --git a/packages/models/src/Domain/Runtime/Display/Search/SearchUtilities.ts b/packages/models/src/Domain/Runtime/Display/Search/SearchUtilities.ts index b8b2db815..7f97237a1 100644 --- a/packages/models/src/Domain/Runtime/Display/Search/SearchUtilities.ts +++ b/packages/models/src/Domain/Runtime/Display/Search/SearchUtilities.ts @@ -1,6 +1,6 @@ import { ContentType } from '@standardnotes/common' import { SNTag } from '../../../Syncable/Tag' -import { FilterDisplayOptions } from '../DisplayOptions' +import { NotesAndFilesDisplayOptions } from '../DisplayOptions' import { computeFiltersForDisplayOptions } from '../DisplayOptionsToFilters' import { SearchableItem } from './SearchableItem' import { ReferenceLookupCollection, ItemFilter, SearchQuery, SearchableDecryptedItem } from './Types' @@ -13,8 +13,8 @@ enum MatchResult { Uuid = 5, } -export function itemsMatchingOptions( - options: FilterDisplayOptions, +export function notesAndFilesMatchingOptions( + options: NotesAndFilesDisplayOptions, fromItems: SearchableDecryptedItem[], collection: ReferenceLookupCollection, ): SearchableItem[] { diff --git a/packages/models/src/Domain/Runtime/Display/Validator/CollectionCriteriaValidator.ts b/packages/models/src/Domain/Runtime/Display/Validator/CollectionCriteriaValidator.ts new file mode 100644 index 000000000..d82a56df7 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/Validator/CollectionCriteriaValidator.ts @@ -0,0 +1,11 @@ +import { ItemInterface } from '../../../Abstract/Item' +import { ReadonlyItemCollection } from '../Types' +import { CriteriaValidatorInterface } from './CriteriaValidatorInterface' + +export class CollectionCriteriaValidator implements CriteriaValidatorInterface { + constructor(private collection: ReadonlyItemCollection, private element: ItemInterface) {} + + public passes(): boolean { + return this.collection.has(this.element.uuid) + } +} diff --git a/packages/models/src/Domain/Runtime/Display/Validator/CriteriaValidatorInterface.ts b/packages/models/src/Domain/Runtime/Display/Validator/CriteriaValidatorInterface.ts new file mode 100644 index 000000000..4b8ecc808 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/Validator/CriteriaValidatorInterface.ts @@ -0,0 +1,3 @@ +export interface CriteriaValidatorInterface { + passes(): boolean +} diff --git a/packages/models/src/Domain/Runtime/Display/Validator/CustomFilterCriteriaValidator.ts b/packages/models/src/Domain/Runtime/Display/Validator/CustomFilterCriteriaValidator.ts new file mode 100644 index 000000000..16050c77a --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/Validator/CustomFilterCriteriaValidator.ts @@ -0,0 +1,11 @@ +import { DecryptedItemInterface } from '../../../Abstract/Item' +import { DisplayControllerCustomFilter } from '../Types' +import { CriteriaValidatorInterface } from './CriteriaValidatorInterface' + +export class CustomFilterCriteriaValidator implements CriteriaValidatorInterface { + constructor(private customFilter: DisplayControllerCustomFilter, private element: DecryptedItemInterface) {} + + public passes(): boolean { + return this.customFilter(this.element) + } +} diff --git a/packages/models/src/Domain/Runtime/Display/Validator/ExcludeVaultsCriteriaValidator.ts b/packages/models/src/Domain/Runtime/Display/Validator/ExcludeVaultsCriteriaValidator.ts new file mode 100644 index 000000000..ea781161a --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/Validator/ExcludeVaultsCriteriaValidator.ts @@ -0,0 +1,15 @@ +import { CriteriaValidatorInterface } from './CriteriaValidatorInterface' +import { DecryptedItemInterface } from '../../../Abstract/Item' +import { KeySystemIdentifier } from '../../../Syncable/KeySystemRootKey/KeySystemIdentifier' + +export class ExcludeVaultsCriteriaValidator implements CriteriaValidatorInterface { + constructor(private excludeVaults: KeySystemIdentifier[], private element: DecryptedItemInterface) {} + + public passes(): boolean { + const doesElementBelongToExcludedVault = this.excludeVaults.some( + (vault) => this.element.key_system_identifier === vault, + ) + + return !doesElementBelongToExcludedVault + } +} diff --git a/packages/models/src/Domain/Runtime/Display/Validator/ExclusiveVaultCriteriaValidator.ts b/packages/models/src/Domain/Runtime/Display/Validator/ExclusiveVaultCriteriaValidator.ts new file mode 100644 index 000000000..7bc65241f --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/Validator/ExclusiveVaultCriteriaValidator.ts @@ -0,0 +1,11 @@ +import { CriteriaValidatorInterface } from './CriteriaValidatorInterface' +import { DecryptedItemInterface } from '../../../Abstract/Item' +import { KeySystemIdentifier } from '../../../Syncable/KeySystemRootKey/KeySystemIdentifier' + +export class ExclusiveVaultCriteriaValidator implements CriteriaValidatorInterface { + constructor(private exclusiveVault: KeySystemIdentifier, private element: DecryptedItemInterface) {} + + public passes(): boolean { + return this.element.key_system_identifier === this.exclusiveVault + } +} diff --git a/packages/models/src/Domain/Runtime/Display/Validator/HiddenContentCriteriaValidator.ts b/packages/models/src/Domain/Runtime/Display/Validator/HiddenContentCriteriaValidator.ts new file mode 100644 index 000000000..8ed5b8a2c --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/Validator/HiddenContentCriteriaValidator.ts @@ -0,0 +1,11 @@ +import { DecryptedItemInterface } from './../../../Abstract/Item/Interfaces/DecryptedItem' +import { ContentType } from '@standardnotes/common' +import { CriteriaValidatorInterface } from './CriteriaValidatorInterface' + +export class HiddenContentCriteriaValidator implements CriteriaValidatorInterface { + constructor(private hiddenContentTypes: ContentType[], private element: DecryptedItemInterface) {} + + public passes(): boolean { + return !this.hiddenContentTypes.includes(this.element.content_type) + } +} diff --git a/packages/models/src/Domain/Runtime/Display/VaultDisplayOptions.ts b/packages/models/src/Domain/Runtime/Display/VaultDisplayOptions.ts new file mode 100644 index 000000000..f1fc1f1e7 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/VaultDisplayOptions.ts @@ -0,0 +1,109 @@ +import { VaultListingInterface } from '../../Syncable/VaultListing/VaultListingInterface' +import { uniqueArray } from '@standardnotes/utils' +import { + ExclusioanaryOptions, + ExclusiveOptions, + VaultDisplayOptionsPersistable, + isExclusioanaryOptionsValue, +} from './VaultDisplayOptionsTypes' +import { KeySystemIdentifier } from '../../Syncable/KeySystemRootKey/KeySystemIdentifier' + +function KeySystemIdentifiers(vaults: VaultListingInterface[]): KeySystemIdentifier[] { + return vaults.map((vault) => vault.systemIdentifier) +} + +export class VaultDisplayOptions { + constructor(private readonly options: ExclusioanaryOptions | ExclusiveOptions) {} + + public getOptions(): ExclusioanaryOptions | ExclusiveOptions { + return this.options + } + + public getExclusivelyShownVault(): KeySystemIdentifier { + if (isExclusioanaryOptionsValue(this.options)) { + throw new Error('Not in exclusive display mode') + } + + return this.options.exclusive + } + + public isInExclusiveDisplayMode(): boolean { + return !isExclusioanaryOptionsValue(this.options) + } + + public isVaultExplicitelyExcluded(vault: VaultListingInterface): boolean { + if (isExclusioanaryOptionsValue(this.options)) { + return this.options.exclude.some((excludedVault) => excludedVault === vault.systemIdentifier) + } else if (this.options.exclusive) { + return this.options.exclusive !== vault.systemIdentifier + } + + throw new Error('Invalid vault display options') + } + + isVaultExclusivelyShown(vault: VaultListingInterface): boolean { + return !isExclusioanaryOptionsValue(this.options) && this.options.exclusive === vault.systemIdentifier + } + + isVaultDisabledOrLocked(vault: VaultListingInterface): boolean { + if (isExclusioanaryOptionsValue(this.options)) { + const matchingLocked = this.options.locked.find((lockedVault) => lockedVault === vault.systemIdentifier) + if (matchingLocked) { + return true + } + } + + return this.isVaultExplicitelyExcluded(vault) + } + + getPersistableValue(): VaultDisplayOptionsPersistable { + return this.options + } + + newOptionsByIntakingLockedVaults(lockedVaults: VaultListingInterface[]): VaultDisplayOptions { + if (isExclusioanaryOptionsValue(this.options)) { + return new VaultDisplayOptions({ exclude: this.options.exclude, locked: KeySystemIdentifiers(lockedVaults) }) + } else { + return new VaultDisplayOptions({ exclusive: this.options.exclusive }) + } + } + + newOptionsByExcludingVault(vault: VaultListingInterface, lockedVaults: VaultListingInterface[]): VaultDisplayOptions { + return this.newOptionsByExcludingVaults([vault], lockedVaults) + } + + newOptionsByExcludingVaults( + vaults: VaultListingInterface[], + lockedVaults: VaultListingInterface[], + ): VaultDisplayOptions { + if (isExclusioanaryOptionsValue(this.options)) { + return new VaultDisplayOptions({ + exclude: uniqueArray([...this.options.exclude, ...KeySystemIdentifiers(vaults)]), + locked: KeySystemIdentifiers(lockedVaults), + }) + } else { + return new VaultDisplayOptions({ + exclude: KeySystemIdentifiers(vaults), + locked: KeySystemIdentifiers(lockedVaults), + }) + } + } + + newOptionsByUnexcludingVault( + vault: VaultListingInterface, + lockedVaults: VaultListingInterface[], + ): VaultDisplayOptions { + if (isExclusioanaryOptionsValue(this.options)) { + return new VaultDisplayOptions({ + exclude: this.options.exclude.filter((excludedVault) => excludedVault !== vault.systemIdentifier), + locked: KeySystemIdentifiers(lockedVaults), + }) + } else { + return new VaultDisplayOptions({ exclude: [], locked: KeySystemIdentifiers(lockedVaults) }) + } + } + + static FromPersistableValue(value: VaultDisplayOptionsPersistable): VaultDisplayOptions { + return new VaultDisplayOptions(value) + } +} diff --git a/packages/models/src/Domain/Runtime/Display/VaultDisplayOptionsTypes.ts b/packages/models/src/Domain/Runtime/Display/VaultDisplayOptionsTypes.ts new file mode 100644 index 000000000..78a964fad --- /dev/null +++ b/packages/models/src/Domain/Runtime/Display/VaultDisplayOptionsTypes.ts @@ -0,0 +1,12 @@ +import { KeySystemIdentifier } from '../../Syncable/KeySystemRootKey/KeySystemIdentifier' + +export type ExclusioanaryOptions = { exclude: KeySystemIdentifier[]; locked: KeySystemIdentifier[] } +export type ExclusiveOptions = { exclusive: KeySystemIdentifier } + +export function isExclusioanaryOptionsValue( + options: ExclusioanaryOptions | ExclusiveOptions, +): options is ExclusioanaryOptions { + return 'exclude' in options || 'locked' in options +} + +export type VaultDisplayOptionsPersistable = ExclusioanaryOptions | ExclusiveOptions diff --git a/packages/models/src/Domain/Runtime/Display/index.ts b/packages/models/src/Domain/Runtime/Display/index.ts index 6e66f2c27..dd86731f0 100644 --- a/packages/models/src/Domain/Runtime/Display/index.ts +++ b/packages/models/src/Domain/Runtime/Display/index.ts @@ -6,3 +6,5 @@ export * from './Search/SearchableItem' export * from './Search/SearchUtilities' export * from './Search/Types' export * from './Types' +export * from './VaultDisplayOptions' +export * from './VaultDisplayOptionsTypes' diff --git a/packages/models/src/Domain/Runtime/Encryption/ContentTypeUsesKeySystemRootKeyEncryption.ts b/packages/models/src/Domain/Runtime/Encryption/ContentTypeUsesKeySystemRootKeyEncryption.ts new file mode 100644 index 000000000..a2471569d --- /dev/null +++ b/packages/models/src/Domain/Runtime/Encryption/ContentTypeUsesKeySystemRootKeyEncryption.ts @@ -0,0 +1,5 @@ +import { ContentType } from '@standardnotes/common' + +export function ContentTypeUsesKeySystemRootKeyEncryption(contentType: ContentType): boolean { + return contentType === ContentType.KeySystemItemsKey +} diff --git a/packages/models/src/Domain/Runtime/Encryption/ContentTypeUsesRootKeyEncryption.ts b/packages/models/src/Domain/Runtime/Encryption/ContentTypeUsesRootKeyEncryption.ts new file mode 100644 index 000000000..727036543 --- /dev/null +++ b/packages/models/src/Domain/Runtime/Encryption/ContentTypeUsesRootKeyEncryption.ts @@ -0,0 +1,6 @@ +import { ContentType } from '@standardnotes/common' +import { ContentTypesUsingRootKeyEncryption } from './ContentTypesUsingRootKeyEncryption' + +export function ContentTypeUsesRootKeyEncryption(contentType: ContentType): boolean { + return ContentTypesUsingRootKeyEncryption().includes(contentType) +} diff --git a/packages/models/src/Domain/Runtime/Encryption/ContentTypesUsingRootKeyEncryption.ts b/packages/models/src/Domain/Runtime/Encryption/ContentTypesUsingRootKeyEncryption.ts new file mode 100644 index 000000000..46c2b82cb --- /dev/null +++ b/packages/models/src/Domain/Runtime/Encryption/ContentTypesUsingRootKeyEncryption.ts @@ -0,0 +1,11 @@ +import { ContentType } from '@standardnotes/common' + +export function ContentTypesUsingRootKeyEncryption(): ContentType[] { + return [ + ContentType.RootKey, + ContentType.ItemsKey, + ContentType.EncryptedStorage, + ContentType.TrustedContact, + ContentType.KeySystemRootKey, + ] +} diff --git a/packages/models/src/Domain/Runtime/Encryption/PersistentSignatureData.ts b/packages/models/src/Domain/Runtime/Encryption/PersistentSignatureData.ts new file mode 100644 index 000000000..4c1b7b8cc --- /dev/null +++ b/packages/models/src/Domain/Runtime/Encryption/PersistentSignatureData.ts @@ -0,0 +1,19 @@ +export type PersistentSignatureData = + | { + required: true + contentHash: string + result: { + passes: boolean + publicKey: string + signature: string + } + } + | { + required: false + contentHash: string + result?: { + passes: boolean + publicKey: string + signature: string + } + } diff --git a/packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyContent.ts b/packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyContent.ts new file mode 100644 index 000000000..7fb19d50c --- /dev/null +++ b/packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyContent.ts @@ -0,0 +1,11 @@ +import { ProtocolVersion } from '@standardnotes/common' +import { ItemContent, SpecializedContent } from '../../Abstract/Content/ItemContent' + +export interface KeySystemItemsKeyContentSpecialized extends SpecializedContent { + version: ProtocolVersion + creationTimestamp: number + itemsKey: string + rootKeyToken: string +} + +export type KeySystemItemsKeyContent = KeySystemItemsKeyContentSpecialized & ItemContent diff --git a/packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyInterface.ts b/packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyInterface.ts new file mode 100644 index 000000000..46d7c23e2 --- /dev/null +++ b/packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyInterface.ts @@ -0,0 +1,11 @@ +import { ProtocolVersion } from '@standardnotes/common' +import { DecryptedItemInterface } from '../../Abstract/Item/Interfaces/DecryptedItem' +import { KeySystemItemsKeyContent } from './KeySystemItemsKeyContent' + +export interface KeySystemItemsKeyInterface extends DecryptedItemInterface { + readonly creationTimestamp: number + readonly rootKeyToken: string + + get keyVersion(): ProtocolVersion + get itemsKey(): string +} diff --git a/packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyMutatorInterface.ts b/packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyMutatorInterface.ts new file mode 100644 index 000000000..7e3c9041e --- /dev/null +++ b/packages/models/src/Domain/Syncable/KeySystemItemsKey/KeySystemItemsKeyMutatorInterface.ts @@ -0,0 +1,4 @@ +import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator' + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface KeySystemItemsKeyMutatorInterface extends DecryptedItemMutator {} diff --git a/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemIdentifier.ts b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemIdentifier.ts new file mode 100644 index 000000000..6cd652860 --- /dev/null +++ b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemIdentifier.ts @@ -0,0 +1 @@ +export type KeySystemIdentifier = string diff --git a/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKey.ts b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKey.ts new file mode 100644 index 000000000..d7207efe2 --- /dev/null +++ b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKey.ts @@ -0,0 +1,54 @@ +import { ContentType, ProtocolVersion } from '@standardnotes/common' +import { ConflictStrategy, DecryptedItem } from '../../Abstract/Item' +import { DecryptedPayloadInterface } from '../../Abstract/Payload' +import { HistoryEntryInterface } from '../../Runtime/History' +import { KeySystemRootKeyContent } from './KeySystemRootKeyContent' +import { KeySystemRootKeyInterface } from './KeySystemRootKeyInterface' +import { KeySystemIdentifier } from './KeySystemIdentifier' +import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface' + +export function isKeySystemRootKey(x: { content_type: ContentType }): x is KeySystemRootKey { + return x.content_type === ContentType.KeySystemRootKey +} + +export class KeySystemRootKey extends DecryptedItem implements KeySystemRootKeyInterface { + keyParams: KeySystemRootKeyParamsInterface + systemIdentifier: KeySystemIdentifier + + key: string + keyVersion: ProtocolVersion + token: string + + constructor(payload: DecryptedPayloadInterface) { + super(payload) + + this.keyParams = payload.content.keyParams + this.systemIdentifier = payload.content.systemIdentifier + + this.key = payload.content.key + this.keyVersion = payload.content.keyVersion + this.token = payload.content.token + } + + override strategyWhenConflictingWithItem( + item: KeySystemRootKey, + _previousRevision?: HistoryEntryInterface, + ): ConflictStrategy { + const baseKeyTimestamp = this.keyParams.creationTimestamp + const incomingKeyTimestamp = item.keyParams.creationTimestamp + + return incomingKeyTimestamp > baseKeyTimestamp ? ConflictStrategy.KeepApply : ConflictStrategy.KeepBase + } + + get itemsKey(): string { + return this.key + } + + override get key_system_identifier(): undefined { + return undefined + } + + override get shared_vault_uuid(): undefined { + return undefined + } +} diff --git a/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyContent.ts b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyContent.ts new file mode 100644 index 000000000..4c370dec0 --- /dev/null +++ b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyContent.ts @@ -0,0 +1,16 @@ +import { ProtocolVersion } from '@standardnotes/common' +import { ItemContent } from '../../Abstract/Content/ItemContent' +import { KeySystemIdentifier } from './KeySystemIdentifier' +import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface' + +export type KeySystemRootKeyContentSpecialized = { + keyParams: KeySystemRootKeyParamsInterface + systemIdentifier: KeySystemIdentifier + + key: string + keyVersion: ProtocolVersion + + token: string +} + +export type KeySystemRootKeyContent = KeySystemRootKeyContentSpecialized & ItemContent diff --git a/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyInterface.ts b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyInterface.ts new file mode 100644 index 000000000..a65b5d67c --- /dev/null +++ b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyInterface.ts @@ -0,0 +1,38 @@ +import { ProtocolVersion } from '@standardnotes/common' +import { DecryptedItemInterface } from '../../Abstract/Item/Interfaces/DecryptedItem' +import { KeySystemRootKeyContent } from './KeySystemRootKeyContent' +import { KeySystemIdentifier } from './KeySystemIdentifier' +import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface' + +export interface KeySystemRootKeyInterface extends DecryptedItemInterface { + keyParams: KeySystemRootKeyParamsInterface + + systemIdentifier: KeySystemIdentifier + + key: string + keyVersion: ProtocolVersion + + /** + * A token is passed to all items keys created while this root key was active. + * When determining which items key a client should use to encrypt new items or new changes, + * it should look for items keys which have the current root key token. This prevents + * the server from dictating which items key a client should use, and also prevents a server from withholding + * items keys from sync results, which would otherwise compel a client to choose between its available items keys, + * which may be old or rotated. + * + * This token is part of the encrypted payload of both the root key and corresponding items keys. While not + * necessarily destructive if leaked, it prevents a malicious server from creating a compromised items key for a vault. + */ + token: string + + get itemsKey(): string + + /** + * Key system root keys pertain to a key system, but they are not actually encrypted inside a key system, but rather + * saved as a normal item in the user's account. An item's key_system_identifier tells the cryptographic system which + * keys to use to encrypt, but a key system rootkey's systemIdentifier is just a reference to that identifier that doesn't + * bind the item to a specific vault system's cryptographic keys. + */ + get key_system_identifier(): undefined + get shared_vault_uuid(): undefined +} diff --git a/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyMutator.ts b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyMutator.ts new file mode 100644 index 000000000..9a8e95f51 --- /dev/null +++ b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyMutator.ts @@ -0,0 +1,4 @@ +import { DecryptedItemMutator } from '../../Abstract/Item' +import { KeySystemRootKeyContent } from './KeySystemRootKeyContent' + +export class KeySystemRootKeyMutator extends DecryptedItemMutator {} diff --git a/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyStorageMode.ts b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyStorageMode.ts new file mode 100644 index 000000000..dcab50554 --- /dev/null +++ b/packages/models/src/Domain/Syncable/KeySystemRootKey/KeySystemRootKeyStorageMode.ts @@ -0,0 +1,5 @@ +export enum KeySystemRootKeyStorageMode { + Synced = 'synced', + Local = 'local', + Ephemeral = 'ephemeral', +} diff --git a/packages/models/src/Domain/Syncable/SmartView/SmartViewBuilder.ts b/packages/models/src/Domain/Syncable/SmartView/SmartViewBuilder.ts index b1da2982c..5e492685e 100644 --- a/packages/models/src/Domain/Syncable/SmartView/SmartViewBuilder.ts +++ b/packages/models/src/Domain/Syncable/SmartView/SmartViewBuilder.ts @@ -9,10 +9,10 @@ import { FillItemContent } from '../../Abstract/Content/ItemContent' import { Predicate } from '../../Runtime/Predicate/Predicate' import { CompoundPredicate } from '../../Runtime/Predicate/CompoundPredicate' import { PayloadTimestampDefaults } from '../../Abstract/Payload' -import { FilterDisplayOptions } from '../../Runtime/Display' +import { NotesAndFilesDisplayOptions } from '../../Runtime/Display' import { FileItem } from '../File' -export function BuildSmartViews(options: FilterDisplayOptions): SmartView[] { +export function BuildSmartViews(options: NotesAndFilesDisplayOptions): SmartView[] { const notes = new SmartView( new DecryptedPayload({ uuid: SystemViewId.AllNotes, @@ -100,7 +100,7 @@ export function BuildSmartViews(options: FilterDisplayOptions): SmartView[] { return [notes, files, starred, archived, trash, untagged, conflicts] } -function allNotesPredicate(options: FilterDisplayOptions) { +function allNotesPredicate(options: NotesAndFilesDisplayOptions) { const subPredicates: Predicate[] = [new Predicate('content_type', '=', ContentType.Note)] if (options.includeTrashed === false) { @@ -120,7 +120,7 @@ function allNotesPredicate(options: FilterDisplayOptions) { return predicate } -function filesPredicate(options: FilterDisplayOptions) { +function filesPredicate(options: NotesAndFilesDisplayOptions) { const subPredicates: Predicate[] = [new Predicate('content_type', '=', ContentType.File)] if (options.includeTrashed === false) { @@ -140,7 +140,7 @@ function filesPredicate(options: FilterDisplayOptions) { return predicate } -function archivedNotesPredicate(options: FilterDisplayOptions) { +function archivedNotesPredicate(options: NotesAndFilesDisplayOptions) { const subPredicates: Predicate[] = [ new Predicate('archived', '=', true), new Predicate('content_type', '=', ContentType.Note), @@ -159,7 +159,7 @@ function archivedNotesPredicate(options: FilterDisplayOptions) { return predicate } -function trashedNotesPredicate(options: FilterDisplayOptions) { +function trashedNotesPredicate(options: NotesAndFilesDisplayOptions) { const subPredicates: Predicate[] = [ new Predicate('trashed', '=', true), new Predicate('content_type', '=', ContentType.Note), @@ -178,7 +178,7 @@ function trashedNotesPredicate(options: FilterDisplayOptions) { return predicate } -function untaggedNotesPredicate(options: FilterDisplayOptions) { +function untaggedNotesPredicate(options: NotesAndFilesDisplayOptions) { const subPredicates = [ new Predicate('content_type', '=', ContentType.Note), new Predicate('tagsCount', '=', 0), @@ -197,7 +197,7 @@ function untaggedNotesPredicate(options: FilterDisplayOptions) { return predicate } -function starredNotesPredicate(options: FilterDisplayOptions) { +function starredNotesPredicate(options: NotesAndFilesDisplayOptions) { const subPredicates: Predicate[] = [ new Predicate('starred', '=', true), new Predicate('content_type', '=', ContentType.Note), @@ -216,7 +216,7 @@ function starredNotesPredicate(options: FilterDisplayOptions) { return predicate } -function conflictsPredicate(options: FilterDisplayOptions) { +function conflictsPredicate(options: NotesAndFilesDisplayOptions) { const subPredicates: Predicate[] = [new Predicate('content_type', '=', ContentType.Note)] if (options.includeTrashed === false) { diff --git a/packages/models/src/Domain/Syncable/TrustedContact/PublicKeySet/ContactPublicKeySet.ts b/packages/models/src/Domain/Syncable/TrustedContact/PublicKeySet/ContactPublicKeySet.ts new file mode 100644 index 000000000..c7ea7e622 --- /dev/null +++ b/packages/models/src/Domain/Syncable/TrustedContact/PublicKeySet/ContactPublicKeySet.ts @@ -0,0 +1,73 @@ +import { ContactPublicKeySetInterface } from './ContactPublicKeySetInterface' +import { ContactPublicKeySetJsonInterface } from './ContactPublicKeySetJsonInterface' + +export class ContactPublicKeySet implements ContactPublicKeySetInterface { + encryption: string + signing: string + timestamp: Date + isRevoked: boolean + previousKeySet?: ContactPublicKeySet + + constructor( + encryption: string, + signing: string, + timestamp: Date, + isRevoked: boolean, + previousKeySet: ContactPublicKeySet | undefined, + ) { + this.encryption = encryption + this.signing = signing + this.timestamp = timestamp + this.isRevoked = isRevoked + this.previousKeySet = previousKeySet + } + + public findKeySet(params: { + targetEncryptionPublicKey: string + targetSigningPublicKey: string + }): ContactPublicKeySetInterface | undefined { + if (this.encryption === params.targetEncryptionPublicKey && this.signing === params.targetSigningPublicKey) { + return this + } + + if (this.previousKeySet) { + return this.previousKeySet.findKeySet(params) + } + + return undefined + } + + public findKeySetWithSigningKey(signingKey: string): ContactPublicKeySetInterface | undefined { + if (this.signing === signingKey) { + return this + } + + if (this.previousKeySet) { + return this.previousKeySet.findKeySetWithSigningKey(signingKey) + } + + return undefined + } + + findKeySetWithPublicKey(publicKey: string): ContactPublicKeySetInterface | undefined { + if (this.encryption === publicKey) { + return this + } + + if (this.previousKeySet) { + return this.previousKeySet.findKeySetWithPublicKey(publicKey) + } + + return undefined + } + + static FromJson(json: ContactPublicKeySetJsonInterface): ContactPublicKeySetInterface { + return new ContactPublicKeySet( + json.encryption, + json.signing, + new Date(json.timestamp), + json.isRevoked, + json.previousKeySet ? ContactPublicKeySet.FromJson(json.previousKeySet) : undefined, + ) + } +} diff --git a/packages/models/src/Domain/Syncable/TrustedContact/PublicKeySet/ContactPublicKeySetInterface.ts b/packages/models/src/Domain/Syncable/TrustedContact/PublicKeySet/ContactPublicKeySetInterface.ts new file mode 100644 index 000000000..031aeb779 --- /dev/null +++ b/packages/models/src/Domain/Syncable/TrustedContact/PublicKeySet/ContactPublicKeySetInterface.ts @@ -0,0 +1,15 @@ +export interface ContactPublicKeySetInterface { + encryption: string + signing: string + timestamp: Date + isRevoked: boolean + previousKeySet?: ContactPublicKeySetInterface + + findKeySet(params: { + targetEncryptionPublicKey: string + targetSigningPublicKey: string + }): ContactPublicKeySetInterface | undefined + + findKeySetWithPublicKey(publicKey: string): ContactPublicKeySetInterface | undefined + findKeySetWithSigningKey(signingKey: string): ContactPublicKeySetInterface | undefined +} diff --git a/packages/models/src/Domain/Syncable/TrustedContact/PublicKeySet/ContactPublicKeySetJsonInterface.ts b/packages/models/src/Domain/Syncable/TrustedContact/PublicKeySet/ContactPublicKeySetJsonInterface.ts new file mode 100644 index 000000000..667ba4526 --- /dev/null +++ b/packages/models/src/Domain/Syncable/TrustedContact/PublicKeySet/ContactPublicKeySetJsonInterface.ts @@ -0,0 +1,7 @@ +export interface ContactPublicKeySetJsonInterface { + encryption: string + signing: string + timestamp: Date + isRevoked: boolean + previousKeySet?: ContactPublicKeySetJsonInterface +} diff --git a/packages/models/src/Domain/Syncable/TrustedContact/PublicKeySet/FindPublicKeySetResult.ts b/packages/models/src/Domain/Syncable/TrustedContact/PublicKeySet/FindPublicKeySetResult.ts new file mode 100644 index 000000000..9860a7549 --- /dev/null +++ b/packages/models/src/Domain/Syncable/TrustedContact/PublicKeySet/FindPublicKeySetResult.ts @@ -0,0 +1,8 @@ +import { ContactPublicKeySetInterface } from './ContactPublicKeySetInterface' + +export type FindPublicKeySetResult = + | { + publicKeySet: ContactPublicKeySetInterface + current: boolean + } + | undefined diff --git a/packages/models/src/Domain/Syncable/TrustedContact/TrustedContact.ts b/packages/models/src/Domain/Syncable/TrustedContact/TrustedContact.ts new file mode 100644 index 000000000..0bafb968f --- /dev/null +++ b/packages/models/src/Domain/Syncable/TrustedContact/TrustedContact.ts @@ -0,0 +1,77 @@ +import { ConflictStrategy, DecryptedItem, DecryptedItemInterface } from '../../Abstract/Item' +import { DecryptedPayloadInterface } from '../../Abstract/Payload' +import { HistoryEntryInterface } from '../../Runtime/History' +import { TrustedContactContent } from './TrustedContactContent' +import { TrustedContactInterface } from './TrustedContactInterface' +import { FindPublicKeySetResult } from './PublicKeySet/FindPublicKeySetResult' +import { ContactPublicKeySet } from './PublicKeySet/ContactPublicKeySet' +import { ContactPublicKeySetInterface } from './PublicKeySet/ContactPublicKeySetInterface' +import { Predicate } from '../../Runtime/Predicate/Predicate' + +export class TrustedContact extends DecryptedItem implements TrustedContactInterface { + static singletonPredicate = new Predicate('isMe', '=', true) + + name: string + contactUuid: string + publicKeySet: ContactPublicKeySetInterface + isMe: boolean + + constructor(payload: DecryptedPayloadInterface) { + super(payload) + + this.name = payload.content.name + this.contactUuid = payload.content.contactUuid + this.publicKeySet = ContactPublicKeySet.FromJson(payload.content.publicKeySet) + this.isMe = payload.content.isMe + } + + override get isSingleton(): true { + return true + } + + override singletonPredicate(): Predicate { + return TrustedContact.singletonPredicate + } + + public findKeySet(params: { + targetEncryptionPublicKey: string + targetSigningPublicKey: string + }): FindPublicKeySetResult { + const set = this.publicKeySet.findKeySet(params) + if (!set) { + return undefined + } + + return { + publicKeySet: set, + current: set === this.publicKeySet, + } + } + + isPublicKeyTrusted(encryptionPublicKey: string): boolean { + const keySet = this.publicKeySet.findKeySetWithPublicKey(encryptionPublicKey) + + if (keySet && !keySet.isRevoked) { + return true + } + + return false + } + + isSigningKeyTrusted(signingKey: string): boolean { + const keySet = this.publicKeySet.findKeySetWithSigningKey(signingKey) + + if (keySet && !keySet.isRevoked) { + return true + } + + return false + } + + override strategyWhenConflictingWithItem( + _item: DecryptedItemInterface, + _previousRevision?: HistoryEntryInterface, + ): ConflictStrategy { + return ConflictStrategy.KeepBase + } +} diff --git a/packages/models/src/Domain/Syncable/TrustedContact/TrustedContactContent.ts b/packages/models/src/Domain/Syncable/TrustedContact/TrustedContactContent.ts new file mode 100644 index 000000000..2cc5406d5 --- /dev/null +++ b/packages/models/src/Domain/Syncable/TrustedContact/TrustedContactContent.ts @@ -0,0 +1,11 @@ +import { ItemContent } from '../../Abstract/Content/ItemContent' +import { ContactPublicKeySetInterface } from './PublicKeySet/ContactPublicKeySetInterface' + +export type TrustedContactContentSpecialized = { + name: string + contactUuid: string + publicKeySet: ContactPublicKeySetInterface + isMe: boolean +} + +export type TrustedContactContent = TrustedContactContentSpecialized & ItemContent diff --git a/packages/models/src/Domain/Syncable/TrustedContact/TrustedContactInterface.ts b/packages/models/src/Domain/Syncable/TrustedContact/TrustedContactInterface.ts new file mode 100644 index 000000000..5cc5841d5 --- /dev/null +++ b/packages/models/src/Domain/Syncable/TrustedContact/TrustedContactInterface.ts @@ -0,0 +1,16 @@ +import { DecryptedItemInterface } from '../../Abstract/Item/Interfaces/DecryptedItem' +import { FindPublicKeySetResult } from './PublicKeySet/FindPublicKeySetResult' +import { TrustedContactContent } from './TrustedContactContent' +import { ContactPublicKeySetInterface } from './PublicKeySet/ContactPublicKeySetInterface' + +export interface TrustedContactInterface extends DecryptedItemInterface { + name: string + contactUuid: string + publicKeySet: ContactPublicKeySetInterface + isMe: boolean + + findKeySet(params: { targetEncryptionPublicKey: string; targetSigningPublicKey: string }): FindPublicKeySetResult + + isPublicKeyTrusted(encryptionPublicKey: string): boolean + isSigningKeyTrusted(signingKey: string): boolean +} diff --git a/packages/models/src/Domain/Syncable/TrustedContact/TrustedContactMutator.ts b/packages/models/src/Domain/Syncable/TrustedContact/TrustedContactMutator.ts new file mode 100644 index 000000000..56ddc9f9c --- /dev/null +++ b/packages/models/src/Domain/Syncable/TrustedContact/TrustedContactMutator.ts @@ -0,0 +1,26 @@ +import { DecryptedItemMutator } from '../../Abstract/Item' +import { TrustedContactContent } from './TrustedContactContent' +import { TrustedContactInterface } from './TrustedContactInterface' +import { ContactPublicKeySet } from './PublicKeySet/ContactPublicKeySet' + +export class TrustedContactMutator extends DecryptedItemMutator { + set name(newName: string) { + this.mutableContent.name = newName + } + + addPublicKey(params: { encryption: string; signing: string }): void { + const newKey = new ContactPublicKeySet( + params.encryption, + params.signing, + new Date(), + false, + this.immutableItem.publicKeySet, + ) + + this.mutableContent.publicKeySet = newKey + } + + replacePublicKeySet(publicKeySet: ContactPublicKeySet): void { + this.mutableContent.publicKeySet = publicKeySet + } +} diff --git a/packages/models/src/Domain/Syncable/VaultListing/VaultListing.ts b/packages/models/src/Domain/Syncable/VaultListing/VaultListing.ts new file mode 100644 index 000000000..92f316d3f --- /dev/null +++ b/packages/models/src/Domain/Syncable/VaultListing/VaultListing.ts @@ -0,0 +1,62 @@ +import { ConflictStrategy, DecryptedItem } from '../../Abstract/Item' +import { DecryptedPayloadInterface } from '../../Abstract/Payload' +import { HistoryEntryInterface } from '../../Runtime/History' +import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface' +import { KeySystemRootKeyPasswordType } from '../../Local/KeyParams/KeySystemRootKeyPasswordType' +import { SharedVaultListingInterface, VaultListingInterface } from './VaultListingInterface' +import { VaultListingContent } from './VaultListingContent' +import { KeySystemRootKeyStorageMode } from '../KeySystemRootKey/KeySystemRootKeyStorageMode' +import { VaultListingSharingInfo } from './VaultListingSharingInfo' +import { KeySystemIdentifier } from '../KeySystemRootKey/KeySystemIdentifier' + +export class VaultListing extends DecryptedItem implements VaultListingInterface { + systemIdentifier: KeySystemIdentifier + + rootKeyParams: KeySystemRootKeyParamsInterface + keyStorageMode: KeySystemRootKeyStorageMode + + name: string + description?: string + + sharing?: VaultListingSharingInfo + + constructor(payload: DecryptedPayloadInterface) { + super(payload) + + this.systemIdentifier = payload.content.systemIdentifier + + this.rootKeyParams = payload.content.rootKeyParams + this.keyStorageMode = payload.content.keyStorageMode + + this.name = payload.content.name + this.description = payload.content.description + + this.sharing = payload.content.sharing + } + + override strategyWhenConflictingWithItem( + item: VaultListing, + _previousRevision?: HistoryEntryInterface, + ): ConflictStrategy { + const baseKeyTimestamp = this.rootKeyParams.creationTimestamp + const incomingKeyTimestamp = item.rootKeyParams.creationTimestamp + + return incomingKeyTimestamp > baseKeyTimestamp ? ConflictStrategy.KeepApply : ConflictStrategy.KeepBase + } + + get keyPasswordType(): KeySystemRootKeyPasswordType { + return this.rootKeyParams.passwordType + } + + isSharedVaultListing(): this is SharedVaultListingInterface { + return this.sharing != undefined + } + + override get key_system_identifier(): undefined { + return undefined + } + + override get shared_vault_uuid(): undefined { + return undefined + } +} diff --git a/packages/models/src/Domain/Syncable/VaultListing/VaultListingContent.ts b/packages/models/src/Domain/Syncable/VaultListing/VaultListingContent.ts new file mode 100644 index 000000000..e6022acda --- /dev/null +++ b/packages/models/src/Domain/Syncable/VaultListing/VaultListingContent.ts @@ -0,0 +1,19 @@ +import { ItemContent, SpecializedContent } from '../../Abstract/Content/ItemContent' +import { KeySystemIdentifier } from '../KeySystemRootKey/KeySystemIdentifier' +import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface' +import { KeySystemRootKeyStorageMode } from '../KeySystemRootKey/KeySystemRootKeyStorageMode' +import { VaultListingSharingInfo } from './VaultListingSharingInfo' + +export interface VaultListingContentSpecialized extends SpecializedContent { + systemIdentifier: KeySystemIdentifier + + rootKeyParams: KeySystemRootKeyParamsInterface + keyStorageMode: KeySystemRootKeyStorageMode + + name: string + description?: string + + sharing?: VaultListingSharingInfo +} + +export type VaultListingContent = VaultListingContentSpecialized & ItemContent diff --git a/packages/models/src/Domain/Syncable/VaultListing/VaultListingInterface.ts b/packages/models/src/Domain/Syncable/VaultListing/VaultListingInterface.ts new file mode 100644 index 000000000..709a3c933 --- /dev/null +++ b/packages/models/src/Domain/Syncable/VaultListing/VaultListingInterface.ts @@ -0,0 +1,29 @@ +import { KeySystemIdentifier } from '../KeySystemRootKey/KeySystemIdentifier' +import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface' +import { KeySystemRootKeyPasswordType } from '../../Local/KeyParams/KeySystemRootKeyPasswordType' +import { KeySystemRootKeyStorageMode } from '../KeySystemRootKey/KeySystemRootKeyStorageMode' +import { VaultListingSharingInfo } from './VaultListingSharingInfo' +import { VaultListingContent } from './VaultListingContent' +import { DecryptedItemInterface } from '../../Abstract/Item' + +export interface VaultListingInterface extends DecryptedItemInterface { + systemIdentifier: KeySystemIdentifier + + rootKeyParams: KeySystemRootKeyParamsInterface + keyStorageMode: KeySystemRootKeyStorageMode + + name: string + description?: string + + sharing?: VaultListingSharingInfo + + get keyPasswordType(): KeySystemRootKeyPasswordType + isSharedVaultListing(): this is SharedVaultListingInterface + + get key_system_identifier(): undefined + get shared_vault_uuid(): undefined +} + +export interface SharedVaultListingInterface extends VaultListingInterface { + sharing: VaultListingSharingInfo +} diff --git a/packages/models/src/Domain/Syncable/VaultListing/VaultListingMutator.ts b/packages/models/src/Domain/Syncable/VaultListing/VaultListingMutator.ts new file mode 100644 index 000000000..da6f7b359 --- /dev/null +++ b/packages/models/src/Domain/Syncable/VaultListing/VaultListingMutator.ts @@ -0,0 +1,27 @@ +import { DecryptedItemMutator } from '../../Abstract/Item' +import { KeySystemRootKeyParamsInterface } from '../../Local/KeyParams/KeySystemRootKeyParamsInterface' +import { KeySystemRootKeyStorageMode } from '../KeySystemRootKey/KeySystemRootKeyStorageMode' +import { VaultListingContent } from './VaultListingContent' +import { VaultListingSharingInfo } from './VaultListingSharingInfo' + +export class VaultListingMutator extends DecryptedItemMutator { + set name(name: string) { + this.mutableContent.name = name + } + + set description(description: string | undefined) { + this.mutableContent.description = description + } + + set sharing(sharing: VaultListingSharingInfo | undefined) { + this.mutableContent.sharing = sharing + } + + set rootKeyParams(rootKeyParams: KeySystemRootKeyParamsInterface) { + this.mutableContent.rootKeyParams = rootKeyParams + } + + set keyStorageMode(keyStorageMode: KeySystemRootKeyStorageMode) { + this.mutableContent.keyStorageMode = keyStorageMode + } +} diff --git a/packages/models/src/Domain/Syncable/VaultListing/VaultListingSharingInfo.ts b/packages/models/src/Domain/Syncable/VaultListing/VaultListingSharingInfo.ts new file mode 100644 index 000000000..f35657e32 --- /dev/null +++ b/packages/models/src/Domain/Syncable/VaultListing/VaultListingSharingInfo.ts @@ -0,0 +1,4 @@ +export type VaultListingSharingInfo = { + sharedVaultUuid: string + ownerUserUuid: string +} diff --git a/packages/models/src/Domain/Utilities/Item/ItemGenerator.ts b/packages/models/src/Domain/Utilities/Item/ItemGenerator.ts index 24eaa20a0..92d87ae46 100644 --- a/packages/models/src/Domain/Utilities/Item/ItemGenerator.ts +++ b/packages/models/src/Domain/Utilities/Item/ItemGenerator.ts @@ -23,6 +23,16 @@ import { NoteMutator } from '../../Syncable/Note/NoteMutator' import { DecryptedItemInterface } from '../../Abstract/Item/Interfaces/DecryptedItem' import { ItemContent } from '../../Abstract/Content/ItemContent' import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator' +import { DeletedItem } from '../../Abstract/Item/Implementations/DeletedItem' +import { EncryptedItemInterface } from '../../Abstract/Item/Interfaces/EncryptedItem' +import { DeletedItemInterface } from '../../Abstract/Item/Interfaces/DeletedItem' +import { SmartViewMutator } from '../../Syncable/SmartView' +import { TrustedContact } from '../../Syncable/TrustedContact/TrustedContact' +import { TrustedContactMutator } from '../../Syncable/TrustedContact/TrustedContactMutator' +import { KeySystemRootKey } from '../../Syncable/KeySystemRootKey/KeySystemRootKey' +import { KeySystemRootKeyMutator } from '../../Syncable/KeySystemRootKey/KeySystemRootKeyMutator' +import { VaultListing } from '../../Syncable/VaultListing/VaultListing' +import { VaultListingMutator } from '../../Syncable/VaultListing/VaultListingMutator' import { DeletedPayloadInterface, EncryptedPayloadInterface, @@ -30,10 +40,6 @@ import { isDeletedPayload, isEncryptedPayload, } from '../../Abstract/Payload' -import { DeletedItem } from '../../Abstract/Item/Implementations/DeletedItem' -import { EncryptedItemInterface } from '../../Abstract/Item/Interfaces/EncryptedItem' -import { DeletedItemInterface } from '../../Abstract/Item/Interfaces/DeletedItem' -import { SmartViewMutator } from '../../Syncable/SmartView' type ItemClass = new (payload: DecryptedPayloadInterface) => DecryptedItem @@ -53,6 +59,9 @@ const ContentTypeClassMapping: Partial> = { mutatorClass: ActionsExtensionMutator, }, [ContentType.Component]: { itemClass: SNComponent, mutatorClass: ComponentMutator }, + [ContentType.KeySystemRootKey]: { itemClass: KeySystemRootKey, mutatorClass: KeySystemRootKeyMutator }, + [ContentType.TrustedContact]: { itemClass: TrustedContact, mutatorClass: TrustedContactMutator }, + [ContentType.VaultListing]: { itemClass: VaultListing, mutatorClass: VaultListingMutator }, [ContentType.Editor]: { itemClass: SNEditor }, [ContentType.ExtensionRepo]: { itemClass: SNFeatureRepo }, [ContentType.File]: { itemClass: FileItem, mutatorClass: FileMutator }, @@ -65,13 +74,13 @@ const ContentTypeClassMapping: Partial> = { export function CreateDecryptedMutatorForItem< I extends DecryptedItemInterface, - M extends DecryptedItemMutator = DecryptedItemMutator, + M extends DecryptedItemMutator = DecryptedItemMutator, >(item: I, type: MutationType): M { const lookupValue = ContentTypeClassMapping[item.content_type]?.mutatorClass if (lookupValue) { return new lookupValue(item, type) as M } else { - return new DecryptedItemMutator(item, type) as M + return new DecryptedItemMutator(item, type) as M } } diff --git a/packages/models/src/Domain/Utilities/Test/SpecUtils.ts b/packages/models/src/Domain/Utilities/Test/SpecUtils.ts index 02694c94a..4747d3d65 100644 --- a/packages/models/src/Domain/Utilities/Test/SpecUtils.ts +++ b/packages/models/src/Domain/Utilities/Test/SpecUtils.ts @@ -13,13 +13,13 @@ export const mockUuid = () => { return `${currentId++}` } -export const createNote = (payload?: Partial): SNNote => { +export const createNote = (content?: Partial): SNNote => { return new SNNote( new DecryptedPayload( { uuid: mockUuid(), content_type: ContentType.Note, - content: FillItemContent({ ...payload }), + content: FillItemContent({ ...content }), ...PayloadTimestampDefaults(), }, PayloadSource.Constructor, diff --git a/packages/models/src/Domain/index.ts b/packages/models/src/Domain/index.ts index c95f30c38..c6dcc78dc 100644 --- a/packages/models/src/Domain/index.ts +++ b/packages/models/src/Domain/index.ts @@ -15,6 +15,7 @@ export * from './Abstract/Contextual/ComponentCreate' export * from './Abstract/Contextual/ComponentRetrieved' export * from './Abstract/Contextual/ContextPayload' export * from './Abstract/Contextual/FilteredServerItem' +export * from './Abstract/Contextual/TrustedConflictParams' export * from './Abstract/Contextual/Functions' export * from './Abstract/Contextual/LocalStorage' export * from './Abstract/Contextual/OfflineSyncPush' @@ -25,19 +26,26 @@ export * from './Abstract/Contextual/SessionHistory' export * from './Abstract/Item' export * from './Abstract/Payload' export * from './Abstract/TransferPayload' + export * from './Api/Subscription/Invitation' export * from './Api/Subscription/InvitationStatus' export * from './Api/Subscription/InviteeIdentifierType' export * from './Api/Subscription/InviterIdentifierType' + export * from './Device/Environment' export * from './Device/Platform' + export * from './Local/KeyParams/RootKeyParamsInterface' +export * from './Local/KeyParams/KeySystemRootKeyParamsInterface' +export * from './Local/KeyParams/KeySystemRootKeyPasswordType' export * from './Local/RootKey/KeychainTypes' export * from './Local/RootKey/RootKeyContent' export * from './Local/RootKey/RootKeyInterface' +export * from './Local/RootKey/RootKeyWithKeyPairsInterface' + export * from './Runtime/Collection/CollectionSort' export * from './Runtime/Collection/Item/ItemCollection' -export * from './Runtime/Collection/Item/TagItemsIndex' +export * from './Runtime/Collection/Item/ItemCounter' export * from './Runtime/Collection/Payload/ImmutablePayloadCollection' export * from './Runtime/Collection/Payload/PayloadCollection' export * from './Runtime/Deltas' @@ -57,6 +65,20 @@ export * from './Runtime/Predicate/NotPredicate' export * from './Runtime/Predicate/Operator' export * from './Runtime/Predicate/Predicate' export * from './Runtime/Predicate/Utils' + +export * from './Runtime/AsymmetricMessage/AsymmetricMessagePayload' +export * from './Runtime/AsymmetricMessage/AsymmetricMessagePayloadType' +export * from './Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSenderKeypairChanged' +export * from './Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultInvite' +export * from './Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultMetadataChanged' +export * from './Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageSharedVaultRootKeyChanged' +export * from './Runtime/AsymmetricMessage/MessageTypes/AsymmetricMessageTrustedContactShare' + +export * from './Runtime/Encryption/PersistentSignatureData' +export * from './Runtime/Encryption/ContentTypeUsesRootKeyEncryption' +export * from './Runtime/Encryption/ContentTypesUsingRootKeyEncryption' +export * from './Runtime/Encryption/ContentTypeUsesKeySystemRootKeyEncryption' + export * from './Syncable/ActionsExtension' export * from './Syncable/Component' export * from './Syncable/Editor' @@ -69,6 +91,30 @@ export * from './Syncable/SmartView' export * from './Syncable/Tag' export * from './Syncable/Theme' export * from './Syncable/UserPrefs' + +export * from './Syncable/TrustedContact/TrustedContact' +export * from './Syncable/TrustedContact/TrustedContactMutator' +export * from './Syncable/TrustedContact/TrustedContactContent' +export * from './Syncable/TrustedContact/TrustedContactInterface' +export * from './Syncable/TrustedContact/PublicKeySet/ContactPublicKeySetInterface' +export * from './Syncable/TrustedContact/PublicKeySet/ContactPublicKeySet' + +export * from './Syncable/KeySystemRootKey/KeySystemRootKey' +export * from './Syncable/KeySystemRootKey/KeySystemRootKeyMutator' +export * from './Syncable/KeySystemRootKey/KeySystemRootKeyContent' +export * from './Syncable/KeySystemRootKey/KeySystemRootKeyInterface' +export * from './Syncable/KeySystemRootKey/KeySystemRootKeyStorageMode' + +export * from './Syncable/KeySystemItemsKey/KeySystemItemsKeyInterface' +export * from './Syncable/KeySystemItemsKey/KeySystemItemsKeyContent' +export * from './Syncable/KeySystemItemsKey/KeySystemItemsKeyMutatorInterface' + +export * from './Syncable/VaultListing/VaultListing' +export * from './Syncable/VaultListing/VaultListingContent' +export * from './Syncable/VaultListing/VaultListingInterface' +export * from './Syncable/VaultListing/VaultListingMutator' +export * from './Syncable/VaultListing/VaultListingSharingInfo' + export * from './Utilities/Icon/IconType' export * from './Utilities/Item/FindItem' export * from './Utilities/Item/ItemContentsDiffer' @@ -81,3 +127,4 @@ export * from './Utilities/Payload/PayloadContentsEqual' export * from './Utilities/Payload/PayloadsByAlternatingUuid' export * from './Utilities/Payload/PayloadsByDuplicating' export * from './Utilities/Payload/PayloadSplit' +export * from './Syncable/KeySystemRootKey/KeySystemIdentifier' diff --git a/packages/responses/package.json b/packages/responses/package.json index 912ee92c3..4c6ba490f 100644 --- a/packages/responses/package.json +++ b/packages/responses/package.json @@ -33,7 +33,7 @@ "typescript": "*" }, "dependencies": { - "@standardnotes/common": "^1.46.6", + "@standardnotes/common": "^1.48.3", "@standardnotes/features": "workspace:*", "@standardnotes/security": "^1.7.6", "reflect-metadata": "^0.1.13" diff --git a/packages/responses/src/Domain/AsymmetricMessage/AsymmetricMessageServerHash.ts b/packages/responses/src/Domain/AsymmetricMessage/AsymmetricMessageServerHash.ts new file mode 100644 index 000000000..a22444dcf --- /dev/null +++ b/packages/responses/src/Domain/AsymmetricMessage/AsymmetricMessageServerHash.ts @@ -0,0 +1,8 @@ +export interface AsymmetricMessageServerHash { + uuid: string + user_uuid: string + sender_uuid: string + encrypted_message: string + created_at_timestamp: number + updated_at_timestamp: number +} diff --git a/packages/responses/src/Domain/Error/ClientDisplayableError.ts b/packages/responses/src/Domain/Error/ClientDisplayableError.ts new file mode 100644 index 000000000..e42138d7a --- /dev/null +++ b/packages/responses/src/Domain/Error/ClientDisplayableError.ts @@ -0,0 +1,23 @@ +import { HttpErrorResponse } from '../Http' + +export class ClientDisplayableError { + constructor(public text: string, public title?: string, public tag?: string) { + console.error('Client Displayable Error:', text, title || '', tag || '') + } + + static FromError(error: { message: string; tag?: string }) { + return new ClientDisplayableError(error.message, undefined, error.tag) + } + + static FromString(text: string) { + return new ClientDisplayableError(text) + } + + static FromNetworkError(error: HttpErrorResponse) { + return new ClientDisplayableError(error.data.error.message) + } +} + +export function isClientDisplayableError(error: unknown): error is ClientDisplayableError { + return error instanceof ClientDisplayableError +} diff --git a/packages/responses/src/Domain/Error/ClientError.ts b/packages/responses/src/Domain/Error/ClientError.ts deleted file mode 100644 index fa04f0055..000000000 --- a/packages/responses/src/Domain/Error/ClientError.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class ClientDisplayableError { - constructor(public text: string, public title?: string, public tag?: string) { - console.error('Client Displayable Error:', text, title || '', tag || '') - } - - static FromError(error: { message: string; tag?: string }) { - return new ClientDisplayableError(error.message, undefined, error.tag) - } -} diff --git a/packages/responses/src/Domain/Files/CreateValetTokenPayload.ts b/packages/responses/src/Domain/Files/CreateValetTokenPayload.ts index c2ad37a19..dc92c78ed 100644 --- a/packages/responses/src/Domain/Files/CreateValetTokenPayload.ts +++ b/packages/responses/src/Domain/Files/CreateValetTokenPayload.ts @@ -1,5 +1,7 @@ +import { ValetTokenOperation } from './ValetTokenOperation' + export type CreateValetTokenPayload = { - operation: 'read' | 'write' | 'delete' + operation: ValetTokenOperation resources: Array<{ remoteIdentifier: string unencryptedFileSize?: number diff --git a/packages/responses/src/Domain/Files/MoveFileResponse.ts b/packages/responses/src/Domain/Files/MoveFileResponse.ts new file mode 100644 index 000000000..4b7aa6d32 --- /dev/null +++ b/packages/responses/src/Domain/Files/MoveFileResponse.ts @@ -0,0 +1,3 @@ +export type MoveFileResponse = { + success: boolean +} diff --git a/packages/responses/src/Domain/Files/ValetTokenOperation.ts b/packages/responses/src/Domain/Files/ValetTokenOperation.ts new file mode 100644 index 000000000..ea41f853c --- /dev/null +++ b/packages/responses/src/Domain/Files/ValetTokenOperation.ts @@ -0,0 +1 @@ +export type ValetTokenOperation = 'read' | 'write' | 'delete' | 'move' diff --git a/packages/responses/src/Domain/Http/ErrorTag.ts b/packages/responses/src/Domain/Http/ErrorTag.ts index 715c3a4be..15ab6bc41 100644 --- a/packages/responses/src/Domain/Http/ErrorTag.ts +++ b/packages/responses/src/Domain/Http/ErrorTag.ts @@ -9,6 +9,7 @@ export enum ErrorTag { RevokedSession = 'revoked-session', AuthInvalid = 'invalid-auth', ReadOnlyAccess = 'read-only-access', + ExpiredItemShare = 'expired-item-share', ClientValidationError = 'client-validation-error', ClientCanceledMfa = 'client-canceled-mfa', diff --git a/packages/responses/src/Domain/Http/HttpResponse.ts b/packages/responses/src/Domain/Http/HttpResponse.ts index 0f3536d8b..19af81258 100644 --- a/packages/responses/src/Domain/Http/HttpResponse.ts +++ b/packages/responses/src/Domain/Http/HttpResponse.ts @@ -22,5 +22,5 @@ export interface HttpSuccessResponse extends HttpResponseB export type HttpResponse = HttpErrorResponse | HttpSuccessResponse export function isErrorResponse(response: HttpResponse): response is HttpErrorResponse { - return (response.data as HttpErrorResponseBody)?.error != undefined + return (response.data as HttpErrorResponseBody)?.error != undefined || response.status >= 400 } diff --git a/packages/responses/src/Domain/Item/ApiEndpointParam.ts b/packages/responses/src/Domain/Item/ApiEndpointParam.ts index 007ded326..38ea6e58f 100644 --- a/packages/responses/src/Domain/Item/ApiEndpointParam.ts +++ b/packages/responses/src/Domain/Item/ApiEndpointParam.ts @@ -4,4 +4,5 @@ export enum ApiEndpointParam { SyncDlLimit = 'limit', SyncPayloads = 'items', ApiVersion = 'api', + SharedVaultUuids = 'shared_vault_uuids', } diff --git a/packages/responses/src/Domain/Item/ConflictParams.ts b/packages/responses/src/Domain/Item/ConflictParams.ts index 9530f8595..a2eb6201a 100644 --- a/packages/responses/src/Domain/Item/ConflictParams.ts +++ b/packages/responses/src/Domain/Item/ConflictParams.ts @@ -1,11 +1,98 @@ import { ConflictType } from './ConflictType' import { ServerItemResponse } from './ServerItemResponse' -export type ConflictParams = { +type BaseConflictParams = { type: ConflictType - server_item?: ServerItemResponse - unsaved_item?: ServerItemResponse - - /** @legacay */ - item?: ServerItemResponse + server_item?: T + unsaved_item?: T } + +export type ConflictParamsWithServerItem = BaseConflictParams & { + server_item: T + unsaved_item: never +} + +export type ConflictParamsWithUnsavedItem = BaseConflictParams & { + unsaved_item: T + server_item: never +} + +export type ConflictParamsWithServerAndUnsavedItem = BaseConflictParams & { + server_item: T + unsaved_item: T +} + +export type ConflictConflictingDataParams = BaseConflictParams & { + type: ConflictType.ConflictingData + server_item: T + unsaved_item: never +} + +export type ConflictUuidConflictParams = BaseConflictParams & { + type: ConflictType.UuidConflict + server_item: never + unsaved_item: T +} + +export type ConflictContentTypeErrorParams = BaseConflictParams & { + type: ConflictType.ContentTypeError + server_item: never + unsaved_item: T +} + +export type ConflictContentErrorParams = BaseConflictParams & { + type: ConflictType.ContentError + server_item: never + unsaved_item: T +} + +export type ConflictReadOnlyErrorParams = BaseConflictParams & { + type: ConflictType.ReadOnlyError + server_item: T + unsaved_item: T +} + +export type ConflictUuidErrorParams = BaseConflictParams & { + type: ConflictType.UuidError + server_item: never + unsaved_item: T +} + +export type ConflictSharedVaultNotMemberErrorParams = BaseConflictParams & { + type: ConflictType.SharedVaultNotMemberError + server_item: never + unsaved_item: T +} + +export type ConflictSharedVaultInsufficientPermissionsErrorParams = BaseConflictParams & { + type: ConflictType.SharedVaultInsufficientPermissionsError + unsaved_item: T +} + +export function conflictParamsHasServerItemAndUnsavedItem( + params: BaseConflictParams, +): params is ConflictParamsWithServerAndUnsavedItem { + return params.server_item !== undefined && params.unsaved_item !== undefined +} + +export function conflictParamsHasOnlyServerItem( + params: BaseConflictParams, +): params is ConflictParamsWithServerItem { + return params.server_item !== undefined +} + +export function conflictParamsHasOnlyUnsavedItem( + params: BaseConflictParams, +): params is ConflictParamsWithUnsavedItem { + return params.unsaved_item !== undefined +} + +export type ConflictParams = + | ConflictConflictingDataParams + | ConflictUuidConflictParams + | ConflictContentTypeErrorParams + | ConflictContentErrorParams + | ConflictReadOnlyErrorParams + | ConflictUuidErrorParams + | ConflictSharedVaultNotMemberErrorParams + | ConflictSharedVaultInsufficientPermissionsErrorParams diff --git a/packages/responses/src/Domain/Item/ConflictType.ts b/packages/responses/src/Domain/Item/ConflictType.ts index 3618a24a1..c3f80a782 100644 --- a/packages/responses/src/Domain/Item/ConflictType.ts +++ b/packages/responses/src/Domain/Item/ConflictType.ts @@ -5,5 +5,9 @@ export enum ConflictType { ContentError = 'content_error', ReadOnlyError = 'readonly_error', UuidError = 'uuid_error', - SyncError = 'sync_error', + + SharedVaultSnjsVersionError = 'shared_vault_snjs_version_error', + SharedVaultInsufficientPermissionsError = 'shared_vault_insufficient_permissions_error', + SharedVaultNotMemberError = 'shared_vault_not_member_error', + SharedVaultInvalidState = 'shared_vault_invalid_state', } diff --git a/packages/responses/src/Domain/Item/RawSyncData.ts b/packages/responses/src/Domain/Item/RawSyncData.ts index 9db1004d9..ec8c9bb78 100644 --- a/packages/responses/src/Domain/Item/RawSyncData.ts +++ b/packages/responses/src/Domain/Item/RawSyncData.ts @@ -1,6 +1,10 @@ +import { SharedVaultInviteServerHash } from '../SharedVaults/SharedVaultInviteServerHash' import { ApiEndpointParam } from './ApiEndpointParam' import { ConflictParams } from './ConflictParams' import { ServerItemResponse } from './ServerItemResponse' +import { SharedVaultServerHash } from '../SharedVaults/SharedVaultServerHash' +import { UserEventServerHash } from '../UserEvent/UserEventServerHash' +import { AsymmetricMessageServerHash } from '../AsymmetricMessage/AsymmetricMessageServerHash' export type RawSyncData = { error?: unknown @@ -10,5 +14,9 @@ export type RawSyncData = { saved_items?: ServerItemResponse[] conflicts?: ConflictParams[] unsaved?: ConflictParams[] + shared_vaults?: SharedVaultServerHash[] + shared_vault_invites?: SharedVaultInviteServerHash[] + user_events?: UserEventServerHash[] + asymmetric_messages?: AsymmetricMessageServerHash[] status?: number } diff --git a/packages/responses/src/Domain/Item/ServerItemResponse.ts b/packages/responses/src/Domain/Item/ServerItemResponse.ts index 7d6a27cdc..4221e2858 100644 --- a/packages/responses/src/Domain/Item/ServerItemResponse.ts +++ b/packages/responses/src/Domain/Item/ServerItemResponse.ts @@ -12,4 +12,8 @@ export interface ServerItemResponse { updated_at_timestamp: number updated_at: Date uuid: string + user_uuid: string + shared_vault_uuid: string | undefined + key_system_identifier: string | undefined + last_edited_by_uuid?: string } diff --git a/packages/responses/src/Domain/SharedVaults/SharedVaultInviteServerHash.ts b/packages/responses/src/Domain/SharedVaults/SharedVaultInviteServerHash.ts new file mode 100644 index 000000000..562eea2de --- /dev/null +++ b/packages/responses/src/Domain/SharedVaults/SharedVaultInviteServerHash.ts @@ -0,0 +1,13 @@ +import { AsymmetricMessageServerHash } from '../AsymmetricMessage/AsymmetricMessageServerHash' +import { SharedVaultPermission } from './SharedVaultPermission' + +export interface SharedVaultInviteServerHash extends AsymmetricMessageServerHash { + uuid: string + shared_vault_uuid: string + user_uuid: string + sender_uuid: string + encrypted_message: string + permissions: SharedVaultPermission + created_at_timestamp: number + updated_at_timestamp: number +} diff --git a/packages/responses/src/Domain/SharedVaults/SharedVaultPermission.ts b/packages/responses/src/Domain/SharedVaults/SharedVaultPermission.ts new file mode 100644 index 000000000..877fd4031 --- /dev/null +++ b/packages/responses/src/Domain/SharedVaults/SharedVaultPermission.ts @@ -0,0 +1,5 @@ +export enum SharedVaultPermission { + Read = 'read', + Write = 'write', + Admin = 'admin', +} diff --git a/packages/responses/src/Domain/SharedVaults/SharedVaultServerHash.ts b/packages/responses/src/Domain/SharedVaults/SharedVaultServerHash.ts new file mode 100644 index 000000000..aefd70529 --- /dev/null +++ b/packages/responses/src/Domain/SharedVaults/SharedVaultServerHash.ts @@ -0,0 +1,4 @@ +export interface SharedVaultServerHash { + uuid: string + user_uuid: string +} diff --git a/packages/responses/src/Domain/SharedVaults/SharedVaultUserServerHash.ts b/packages/responses/src/Domain/SharedVaults/SharedVaultUserServerHash.ts new file mode 100644 index 000000000..8b6aa4487 --- /dev/null +++ b/packages/responses/src/Domain/SharedVaults/SharedVaultUserServerHash.ts @@ -0,0 +1,9 @@ +import { SharedVaultPermission } from './SharedVaultPermission' + +export interface SharedVaultUserServerHash { + uuid: string + shared_vault_uuid: string + user_uuid: string + permissions: SharedVaultPermission + updated_at_timestamp: number +} diff --git a/packages/responses/src/Domain/UserEvent/UserEventPayload.ts b/packages/responses/src/Domain/UserEvent/UserEventPayload.ts new file mode 100644 index 000000000..bbb34778e --- /dev/null +++ b/packages/responses/src/Domain/UserEvent/UserEventPayload.ts @@ -0,0 +1,14 @@ +import { UserEventType } from './UserEventType' + +export type UserEventPayload = + | { + eventType: UserEventType.SharedVaultItemRemoved + itemUuid: string + sharedVaultUuid: string + version: string + } + | { + eventType: UserEventType.RemovedFromSharedVault + sharedVaultUuid: string + version: string + } diff --git a/packages/responses/src/Domain/UserEvent/UserEventServerHash.ts b/packages/responses/src/Domain/UserEvent/UserEventServerHash.ts new file mode 100644 index 000000000..d72d42842 --- /dev/null +++ b/packages/responses/src/Domain/UserEvent/UserEventServerHash.ts @@ -0,0 +1,10 @@ +import { UserEventType } from './UserEventType' + +export type UserEventServerHash = { + uuid: string + user_uuid: string + event_type: UserEventType + event_payload: string + created_at_timestamp?: number + updated_at_timestamp?: number +} diff --git a/packages/responses/src/Domain/UserEvent/UserEventType.ts b/packages/responses/src/Domain/UserEvent/UserEventType.ts new file mode 100644 index 000000000..1e9008520 --- /dev/null +++ b/packages/responses/src/Domain/UserEvent/UserEventType.ts @@ -0,0 +1,4 @@ +export enum UserEventType { + SharedVaultItemRemoved = 'shared_vault_item_removed', + RemovedFromSharedVault = 'removed_from_shared_vault', +} diff --git a/packages/responses/src/Domain/index.ts b/packages/responses/src/Domain/index.ts index f926d5b39..4b863fb64 100644 --- a/packages/responses/src/Domain/index.ts +++ b/packages/responses/src/Domain/index.ts @@ -13,7 +13,9 @@ export * from './Auth/SignInData' export * from './Auth/SignInResponse' export * from './Auth/SignOutResponse' export * from './Auth/User' -export * from './Error/ClientError' + +export * from './Error/ClientDisplayableError' + export * from './Files/CloseUploadSessionResponse' export * from './Files/CreateValetTokenPayload' export * from './Files/CreateValetTokenResponse' @@ -21,7 +23,18 @@ export * from './Files/CreateValetTokenResponseData' export * from './Files/DownloadFileChunkResponse' export * from './Files/StartUploadSessionResponse' export * from './Files/UploadFileChunkResponse' +export * from './Files/MoveFileResponse' +export * from './Files/ValetTokenOperation' + export * from './Http' + +export * from './SharedVaults/SharedVaultInviteServerHash' +export * from './SharedVaults/SharedVaultUserServerHash' +export * from './SharedVaults/SharedVaultServerHash' +export * from './SharedVaults/SharedVaultPermission' + +export * from './AsymmetricMessage/AsymmetricMessageServerHash' + export * from './Item/ApiEndpointParam' export * from './Item/CheckIntegrityResponse' export * from './Item/ConflictParams' @@ -31,11 +44,13 @@ export * from './Item/RawSyncData' export * from './Item/RawSyncResponse' export * from './Item/ServerItemResponse' export * from './Item/IntegrityPayload' + export * from './Listed/ActionResponse' export * from './Listed/ListedAccount' export * from './Listed/ListedAccountInfo' export * from './Listed/ListedAccountInfoResponse' export * from './Listed/ListedRegistrationResponse' + export * from './User/AvailableSubscriptions' export * from './User/DeleteSettingResponse' export * from './User/GetAvailableSubscriptionsResponse' @@ -48,3 +63,7 @@ export * from './User/SettingData' export * from './User/UpdateSettingResponse' export * from './User/UserFeaturesData' export * from './User/UserFeaturesResponse' + +export * from './UserEvent/UserEventServerHash' +export * from './UserEvent/UserEventType' +export * from './UserEvent/UserEventPayload' diff --git a/packages/services/package.json b/packages/services/package.json index 593d83a7c..3839a8577 100644 --- a/packages/services/package.json +++ b/packages/services/package.json @@ -11,13 +11,13 @@ "license": "AGPL-3.0-or-later", "scripts": { "tsc": "tsc --project tsconfig.json", - "lint": "eslint src --ext .ts", + "lint": "eslint src --ext .ts && yarn tsc", "lint:fix": "eslint src --ext .ts --fix", - "test": "jest --coverage" + "test": "jest" }, "dependencies": { "@standardnotes/api": "workspace:^", - "@standardnotes/common": "^1.46.4", + "@standardnotes/common": "^1.48.3", "@standardnotes/domain-core": "^1.12.0", "@standardnotes/encryption": "workspace:^", "@standardnotes/features": "workspace:^", diff --git a/packages/services/src/Domain/Alert/AlertService.ts b/packages/services/src/Domain/Alert/AlertService.ts index ec4b4bcdf..5c305fc97 100644 --- a/packages/services/src/Domain/Alert/AlertService.ts +++ b/packages/services/src/Domain/Alert/AlertService.ts @@ -18,8 +18,18 @@ export abstract class AlertService { cancelButtonText?: string, ): Promise + abstract confirmV2(dto: { + text: string + title?: string + confirmButtonText?: string + confirmButtonType?: ButtonType + cancelButtonText?: string + }): Promise + abstract alert(text: string, title?: string, closeButtonText?: string): Promise + abstract alertV2(dto: { text: string; title?: string; closeButtonText?: string }): Promise + abstract blockingDialog(text: string, title?: string): DismissBlockingDialog | Promise showErrorAlert(error: ClientDisplayableError): Promise { diff --git a/packages/services/src/Domain/Application/ApplicationInterface.ts b/packages/services/src/Domain/Application/ApplicationInterface.ts index 3a8f4ce83..bcd7c4bc2 100644 --- a/packages/services/src/Domain/Application/ApplicationInterface.ts +++ b/packages/services/src/Domain/Application/ApplicationInterface.ts @@ -1,5 +1,18 @@ +import { SyncOptions } from './../Sync/SyncOptions' +import { ImportDataReturnType } from './../Mutator/ImportDataUseCase' +import { ChallengeServiceInterface } from './../Challenge/ChallengeServiceInterface' +import { VaultServiceInterface } from './../Vaults/VaultServiceInterface' import { ApplicationIdentifier, ContentType } from '@standardnotes/common' -import { BackupFile, DecryptedItemInterface, ItemStream, Platform, PrefKey, PrefValue } from '@standardnotes/models' +import { + BackupFile, + DecryptedItemInterface, + DecryptedItemMutator, + ItemStream, + PayloadEmitSource, + Platform, + PrefKey, + PrefValue, +} from '@standardnotes/models' import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files' import { AlertService } from '../Alert/AlertService' @@ -9,7 +22,7 @@ import { ApplicationEventCallback } from '../Event/ApplicationEventCallback' import { FeaturesClientInterface } from '../Feature/FeaturesClientInterface' import { SubscriptionClientInterface } from '../Subscription/SubscriptionClientInterface' import { DeviceInterface } from '../Device/DeviceInterface' -import { ItemsClientInterface } from '../Item/ItemsClientInterface' +import { ItemManagerInterface } from '../Item/ItemManagerInterface' import { MutatorClientInterface } from '../Mutator/MutatorClientInterface' import { StorageValueModes } from '../Storage/StorageTypes' @@ -24,6 +37,7 @@ export interface ApplicationInterface { isStarted(): boolean isLaunched(): boolean addEventObserver(callback: ApplicationEventCallback, singleEvent?: ApplicationEvent): () => void + addSingleEventObserver(event: ApplicationEvent, callback: ApplicationEventCallback): () => void hasProtectionSources(): boolean createEncryptedBackupFileForAutomatedDesktopBackups(): Promise createEncryptedBackupFile(): Promise @@ -32,7 +46,7 @@ export interface ApplicationInterface { lock(): Promise softLockBiometrics(): void setValue(key: string, value: unknown, mode?: StorageValueModes): void - getValue(key: string, mode?: StorageValueModes): unknown + getValue(key: string, mode?: StorageValueModes): T removeValue(key: string, mode?: StorageValueModes): Promise isLocked(): Promise getPreference(key: K): PrefValue[K] | undefined @@ -44,15 +58,42 @@ export interface ApplicationInterface { stream: ItemStream, ): () => void hasAccount(): boolean + + importData(data: BackupFile, awaitSync?: boolean): Promise + /** + * Mutates a pre-existing item, marks it as dirty, and syncs it + */ + changeAndSaveItem( + itemToLookupUuidFor: DecryptedItemInterface, + mutate: (mutator: M) => void, + updateTimestamps?: boolean, + emitSource?: PayloadEmitSource, + syncOptions?: SyncOptions, + ): Promise + + /** + * Mutates pre-existing items, marks them as dirty, and syncs + */ + changeAndSaveItems( + itemsToLookupUuidsFor: DecryptedItemInterface[], + mutate: (mutator: M) => void, + updateTimestamps?: boolean, + emitSource?: PayloadEmitSource, + syncOptions?: SyncOptions, + ): Promise + get features(): FeaturesClientInterface get componentManager(): ComponentManagerInterface - get items(): ItemsClientInterface + get items(): ItemManagerInterface get mutator(): MutatorClientInterface get user(): UserClientInterface get files(): FilesClientInterface get subscriptions(): SubscriptionClientInterface get fileBackups(): BackupServiceInterface | undefined get sessions(): SessionsClientInterface + get vaults(): VaultServiceInterface + get challenges(): ChallengeServiceInterface + get alerts(): AlertService readonly identifier: ApplicationIdentifier readonly platform: Platform deviceInterface: DeviceInterface diff --git a/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.spec.ts b/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.spec.ts new file mode 100644 index 000000000..f85892890 --- /dev/null +++ b/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.spec.ts @@ -0,0 +1,63 @@ +import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' +import { HttpServiceInterface } from '@standardnotes/api' +import { AsymmetricMessageService } from './AsymmetricMessageService' +import { ContactServiceInterface } from './../Contacts/ContactServiceInterface' +import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { ItemManagerInterface } from '../Item/ItemManagerInterface' +import { SyncServiceInterface } from '../Sync/SyncServiceInterface' +import { AsymmetricMessageServerHash } from '@standardnotes/responses' +import { AsymmetricMessagePayloadType } from '@standardnotes/models' + +describe('AsymmetricMessageService', () => { + let service: AsymmetricMessageService + + beforeEach(() => { + const http = {} as jest.Mocked + http.delete = jest.fn() + + const encryption = {} as jest.Mocked + const contacts = {} as jest.Mocked + const items = {} as jest.Mocked + const sync = {} as jest.Mocked + const mutator = {} as jest.Mocked + + const eventBus = {} as jest.Mocked + eventBus.addEventHandler = jest.fn() + + service = new AsymmetricMessageService(http, encryption, contacts, items, mutator, sync, eventBus) + }) + + it('should process incoming messages oldest first', async () => { + const messages: AsymmetricMessageServerHash[] = [ + { + uuid: 'newer-message', + user_uuid: '1', + sender_uuid: '2', + encrypted_message: 'encrypted_message', + created_at_timestamp: 2, + updated_at_timestamp: 2, + }, + { + uuid: 'older-message', + user_uuid: '1', + sender_uuid: '2', + encrypted_message: 'encrypted_message', + created_at_timestamp: 1, + updated_at_timestamp: 1, + }, + ] + + const trustedPayloadMock = { type: AsymmetricMessagePayloadType.ContactShare, data: { recipientUuid: '1' } } + + service.getTrustedMessagePayload = jest.fn().mockReturnValue(trustedPayloadMock) + + const handleTrustedContactShareMessageMock = jest.fn() + service.handleTrustedContactShareMessage = handleTrustedContactShareMessageMock + + await service.handleRemoteReceivedAsymmetricMessages(messages) + + expect(handleTrustedContactShareMessageMock.mock.calls[0][0]).toEqual(messages[1]) + expect(handleTrustedContactShareMessageMock.mock.calls[1][0]).toEqual(messages[0]) + }) +}) diff --git a/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.ts b/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.ts new file mode 100644 index 000000000..40d1f2fc7 --- /dev/null +++ b/packages/services/src/Domain/AsymmetricMessage/AsymmetricMessageService.ts @@ -0,0 +1,187 @@ +import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' +import { ContactServiceInterface } from './../Contacts/ContactServiceInterface' +import { AsymmetricMessageServerHash, ClientDisplayableError } from '@standardnotes/responses' +import { SyncEvent, SyncEventReceivedAsymmetricMessagesData } from '../Event/SyncEvent' +import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' +import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface' +import { InternalEventInterface } from '../Internal/InternalEventInterface' +import { AbstractService } from '../Service/AbstractService' +import { GetAsymmetricMessageTrustedPayload } from './UseCase/GetAsymmetricMessageTrustedPayload' +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { + AsymmetricMessageSharedVaultRootKeyChanged, + AsymmetricMessagePayloadType, + AsymmetricMessageSenderKeypairChanged, + AsymmetricMessageTrustedContactShare, + AsymmetricMessagePayload, + AsymmetricMessageSharedVaultMetadataChanged, + VaultListingMutator, +} from '@standardnotes/models' +import { HandleTrustedSharedVaultRootKeyChangedMessage } from './UseCase/HandleTrustedSharedVaultRootKeyChangedMessage' +import { ItemManagerInterface } from '../Item/ItemManagerInterface' +import { SyncServiceInterface } from '../Sync/SyncServiceInterface' +import { SessionEvent } from '../Session/SessionEvent' +import { AsymmetricMessageServer, HttpServiceInterface } from '@standardnotes/api' +import { UserKeyPairChangedEventData } from '../Session/UserKeyPairChangedEventData' +import { SendOwnContactChangeMessage } from './UseCase/SendOwnContactChangeMessage' +import { GetOutboundAsymmetricMessages } from './UseCase/GetOutboundAsymmetricMessages' +import { GetInboundAsymmetricMessages } from './UseCase/GetInboundAsymmetricMessages' +import { GetVaultUseCase } from '../Vaults/UseCase/GetVault' + +export class AsymmetricMessageService extends AbstractService implements InternalEventHandlerInterface { + private messageServer: AsymmetricMessageServer + + constructor( + http: HttpServiceInterface, + private encryption: EncryptionProviderInterface, + private contacts: ContactServiceInterface, + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private sync: SyncServiceInterface, + eventBus: InternalEventBusInterface, + ) { + super(eventBus) + + this.messageServer = new AsymmetricMessageServer(http) + + eventBus.addEventHandler(this, SyncEvent.ReceivedAsymmetricMessages) + eventBus.addEventHandler(this, SessionEvent.UserKeyPairChanged) + } + + async handleEvent(event: InternalEventInterface): Promise { + if (event.type === SessionEvent.UserKeyPairChanged) { + void this.messageServer.deleteAllInboundMessages() + void this.sendOwnContactChangeEventToAllContacts(event.payload as UserKeyPairChangedEventData) + } + + if (event.type === SyncEvent.ReceivedAsymmetricMessages) { + void this.handleRemoteReceivedAsymmetricMessages(event.payload as SyncEventReceivedAsymmetricMessagesData) + } + } + + public async getOutboundMessages(): Promise { + const usecase = new GetOutboundAsymmetricMessages(this.messageServer) + return usecase.execute() + } + + public async getInboundMessages(): Promise { + const usecase = new GetInboundAsymmetricMessages(this.messageServer) + return usecase.execute() + } + + async sendOwnContactChangeEventToAllContacts(data: UserKeyPairChangedEventData): Promise { + if (!data.oldKeyPair || !data.oldSigningKeyPair) { + return + } + + const useCase = new SendOwnContactChangeMessage(this.encryption, this.messageServer) + + const contacts = this.contacts.getAllContacts() + + for (const contact of contacts) { + if (contact.isMe) { + continue + } + + await useCase.execute({ + senderOldKeyPair: data.oldKeyPair, + senderOldSigningKeyPair: data.oldSigningKeyPair, + senderNewKeyPair: data.newKeyPair, + senderNewSigningKeyPair: data.newSigningKeyPair, + contact, + }) + } + } + + async handleRemoteReceivedAsymmetricMessages(messages: AsymmetricMessageServerHash[]): Promise { + if (messages.length === 0) { + return + } + + const sortedMessages = messages.slice().sort((a, b) => a.created_at_timestamp - b.created_at_timestamp) + + for (const message of sortedMessages) { + const trustedMessagePayload = this.getTrustedMessagePayload(message) + if (!trustedMessagePayload) { + continue + } + + if (trustedMessagePayload.data.recipientUuid !== message.user_uuid) { + continue + } + + if (trustedMessagePayload.type === AsymmetricMessagePayloadType.ContactShare) { + await this.handleTrustedContactShareMessage(message, trustedMessagePayload) + } else if (trustedMessagePayload.type === AsymmetricMessagePayloadType.SenderKeypairChanged) { + await this.handleTrustedSenderKeypairChangedMessage(message, trustedMessagePayload) + } else if (trustedMessagePayload.type === AsymmetricMessagePayloadType.SharedVaultRootKeyChanged) { + await this.handleTrustedSharedVaultRootKeyChangedMessage(message, trustedMessagePayload) + } else if (trustedMessagePayload.type === AsymmetricMessagePayloadType.SharedVaultMetadataChanged) { + await this.handleVaultMetadataChangedMessage(message, trustedMessagePayload) + } else if (trustedMessagePayload.type === AsymmetricMessagePayloadType.SharedVaultInvite) { + throw new Error('Shared vault invites payloads are not handled as part of asymmetric messages') + } + + await this.deleteMessageAfterProcessing(message) + } + } + + getTrustedMessagePayload(message: AsymmetricMessageServerHash): AsymmetricMessagePayload | undefined { + const useCase = new GetAsymmetricMessageTrustedPayload(this.encryption, this.contacts) + + return useCase.execute({ + privateKey: this.encryption.getKeyPair().privateKey, + message, + }) + } + + private async deleteMessageAfterProcessing(message: AsymmetricMessageServerHash): Promise { + await this.messageServer.deleteMessage({ messageUuid: message.uuid }) + } + + async handleVaultMetadataChangedMessage( + _message: AsymmetricMessageServerHash, + trustedPayload: AsymmetricMessageSharedVaultMetadataChanged, + ): Promise { + const vault = new GetVaultUseCase(this.items).execute({ sharedVaultUuid: trustedPayload.data.sharedVaultUuid }) + if (!vault) { + return + } + + await this.mutator.changeItem(vault, (mutator) => { + mutator.name = trustedPayload.data.name + mutator.description = trustedPayload.data.description + }) + } + + async handleTrustedContactShareMessage( + _message: AsymmetricMessageServerHash, + trustedPayload: AsymmetricMessageTrustedContactShare, + ): Promise { + await this.contacts.createOrUpdateTrustedContactFromContactShare(trustedPayload.data.trustedContact) + } + + private async handleTrustedSenderKeypairChangedMessage( + message: AsymmetricMessageServerHash, + trustedPayload: AsymmetricMessageSenderKeypairChanged, + ): Promise { + await this.contacts.createOrEditTrustedContact({ + contactUuid: message.sender_uuid, + publicKey: trustedPayload.data.newEncryptionPublicKey, + signingPublicKey: trustedPayload.data.newSigningPublicKey, + }) + } + + private async handleTrustedSharedVaultRootKeyChangedMessage( + _message: AsymmetricMessageServerHash, + trustedPayload: AsymmetricMessageSharedVaultRootKeyChanged, + ): Promise { + const useCase = new HandleTrustedSharedVaultRootKeyChangedMessage( + this.mutator, + this.items, + this.sync, + this.encryption, + ) + await useCase.execute(trustedPayload) + } +} diff --git a/packages/services/src/Domain/AsymmetricMessage/UseCase/GetAsymmetricMessageTrustedPayload.ts b/packages/services/src/Domain/AsymmetricMessage/UseCase/GetAsymmetricMessageTrustedPayload.ts new file mode 100644 index 000000000..99258e02e --- /dev/null +++ b/packages/services/src/Domain/AsymmetricMessage/UseCase/GetAsymmetricMessageTrustedPayload.ts @@ -0,0 +1,23 @@ +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface' +import { AsymmetricMessageServerHash } from '@standardnotes/responses' +import { AsymmetricMessagePayload } from '@standardnotes/models' + +export class GetAsymmetricMessageTrustedPayload { + constructor(private encryption: EncryptionProviderInterface, private contacts: ContactServiceInterface) {} + + execute(dto: { privateKey: string; message: AsymmetricMessageServerHash }): M | undefined { + const trustedContact = this.contacts.findTrustedContact(dto.message.sender_uuid) + if (!trustedContact) { + return undefined + } + + const decryptionResult = this.encryption.asymmetricallyDecryptMessage({ + encryptedString: dto.message.encrypted_message, + trustedSender: trustedContact, + privateKey: dto.privateKey, + }) + + return decryptionResult + } +} diff --git a/packages/services/src/Domain/AsymmetricMessage/UseCase/GetAsymmetricMessageUntrustedPayload.ts b/packages/services/src/Domain/AsymmetricMessage/UseCase/GetAsymmetricMessageUntrustedPayload.ts new file mode 100644 index 000000000..a69d228cb --- /dev/null +++ b/packages/services/src/Domain/AsymmetricMessage/UseCase/GetAsymmetricMessageUntrustedPayload.ts @@ -0,0 +1,17 @@ +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { AsymmetricMessageServerHash } from '@standardnotes/responses' +import { AsymmetricMessagePayload } from '@standardnotes/models' + +export class GetAsymmetricMessageUntrustedPayload { + constructor(private encryption: EncryptionProviderInterface) {} + + execute(dto: { privateKey: string; message: AsymmetricMessageServerHash }): M | undefined { + const decryptionResult = this.encryption.asymmetricallyDecryptMessage({ + encryptedString: dto.message.encrypted_message, + trustedSender: undefined, + privateKey: dto.privateKey, + }) + + return decryptionResult + } +} diff --git a/packages/services/src/Domain/AsymmetricMessage/UseCase/GetInboundAsymmetricMessages.ts b/packages/services/src/Domain/AsymmetricMessage/UseCase/GetInboundAsymmetricMessages.ts new file mode 100644 index 000000000..91bd668ae --- /dev/null +++ b/packages/services/src/Domain/AsymmetricMessage/UseCase/GetInboundAsymmetricMessages.ts @@ -0,0 +1,16 @@ +import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses' +import { AsymmetricMessageServerInterface } from '@standardnotes/api' + +export class GetInboundAsymmetricMessages { + constructor(private messageServer: AsymmetricMessageServerInterface) {} + + async execute(): Promise { + const response = await this.messageServer.getMessages() + + if (isErrorResponse(response)) { + return ClientDisplayableError.FromError(response.data.error) + } + + return response.data.messages + } +} diff --git a/packages/services/src/Domain/AsymmetricMessage/UseCase/GetOutboundAsymmetricMessages.ts b/packages/services/src/Domain/AsymmetricMessage/UseCase/GetOutboundAsymmetricMessages.ts new file mode 100644 index 000000000..5ee97691a --- /dev/null +++ b/packages/services/src/Domain/AsymmetricMessage/UseCase/GetOutboundAsymmetricMessages.ts @@ -0,0 +1,16 @@ +import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses' +import { AsymmetricMessageServerInterface } from '@standardnotes/api' + +export class GetOutboundAsymmetricMessages { + constructor(private messageServer: AsymmetricMessageServerInterface) {} + + async execute(): Promise { + const response = await this.messageServer.getOutboundUserMessages() + + if (isErrorResponse(response)) { + return ClientDisplayableError.FromError(response.data.error) + } + + return response.data.messages + } +} diff --git a/packages/services/src/Domain/AsymmetricMessage/UseCase/HandleTrustedSharedVaultInviteMessage.spec.ts b/packages/services/src/Domain/AsymmetricMessage/UseCase/HandleTrustedSharedVaultInviteMessage.spec.ts new file mode 100644 index 000000000..c789c835f --- /dev/null +++ b/packages/services/src/Domain/AsymmetricMessage/UseCase/HandleTrustedSharedVaultInviteMessage.spec.ts @@ -0,0 +1,67 @@ +import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface' +import { HandleTrustedSharedVaultInviteMessage } from './HandleTrustedSharedVaultInviteMessage' +import { SyncServiceInterface } from '../../Sync/SyncServiceInterface' +import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface' +import { ContentType } from '@standardnotes/common' +import { + AsymmetricMessagePayloadType, + AsymmetricMessageSharedVaultInvite, + KeySystemRootKeyContent, +} from '@standardnotes/models' + +describe('HandleTrustedSharedVaultInviteMessage', () => { + let mutatorMock: jest.Mocked + let syncServiceMock: jest.Mocked + let contactServiceMock: jest.Mocked + + beforeEach(() => { + mutatorMock = { + createItem: jest.fn(), + } as any + + syncServiceMock = { + sync: jest.fn(), + } as any + + contactServiceMock = { + createOrEditTrustedContact: jest.fn(), + } as any + }) + + it('should create root key before creating vault listing so that propagated vault listings do not appear as locked', async () => { + const handleTrustedSharedVaultInviteMessage = new HandleTrustedSharedVaultInviteMessage( + mutatorMock, + syncServiceMock, + contactServiceMock, + ) + + const testMessage = { + type: AsymmetricMessagePayloadType.SharedVaultInvite, + data: { + recipientUuid: 'test-recipient-uuid', + rootKey: { + systemIdentifier: 'test-system-identifier', + } as jest.Mocked, + metadata: { + name: 'test-name', + }, + trustedContacts: [], + }, + } as jest.Mocked + + const sharedVaultUuid = 'test-shared-vault-uuid' + const senderUuid = 'test-sender-uuid' + + await handleTrustedSharedVaultInviteMessage.execute(testMessage, sharedVaultUuid, senderUuid) + + const keySystemRootKeyCallIndex = mutatorMock.createItem.mock.calls.findIndex( + ([contentType]) => contentType === ContentType.KeySystemRootKey, + ) + + const vaultListingCallIndex = mutatorMock.createItem.mock.calls.findIndex( + ([contentType]) => contentType === ContentType.VaultListing, + ) + + expect(keySystemRootKeyCallIndex).toBeLessThan(vaultListingCallIndex) + }) +}) diff --git a/packages/services/src/Domain/AsymmetricMessage/UseCase/HandleTrustedSharedVaultInviteMessage.ts b/packages/services/src/Domain/AsymmetricMessage/UseCase/HandleTrustedSharedVaultInviteMessage.ts new file mode 100644 index 000000000..1a3cebdfb --- /dev/null +++ b/packages/services/src/Domain/AsymmetricMessage/UseCase/HandleTrustedSharedVaultInviteMessage.ts @@ -0,0 +1,64 @@ +import { ContactServiceInterface } from './../../Contacts/ContactServiceInterface' +import { SyncServiceInterface } from '../../Sync/SyncServiceInterface' +import { + KeySystemRootKeyInterface, + AsymmetricMessageSharedVaultInvite, + KeySystemRootKeyContent, + FillItemContent, + FillItemContentSpecialized, + VaultListingContentSpecialized, + KeySystemRootKeyStorageMode, +} from '@standardnotes/models' +import { ContentType } from '@standardnotes/common' +import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface' + +export class HandleTrustedSharedVaultInviteMessage { + constructor( + private mutator: MutatorClientInterface, + private sync: SyncServiceInterface, + private contacts: ContactServiceInterface, + ) {} + + async execute( + message: AsymmetricMessageSharedVaultInvite, + sharedVaultUuid: string, + senderUuid: string, + ): Promise { + const { rootKey: rootKeyContent, trustedContacts, metadata } = message.data + + const content: VaultListingContentSpecialized = { + systemIdentifier: rootKeyContent.systemIdentifier, + rootKeyParams: rootKeyContent.keyParams, + keyStorageMode: KeySystemRootKeyStorageMode.Synced, + name: metadata.name, + description: metadata.description, + sharing: { + sharedVaultUuid: sharedVaultUuid, + ownerUserUuid: senderUuid, + }, + } + + await this.mutator.createItem( + ContentType.KeySystemRootKey, + FillItemContent(rootKeyContent), + true, + ) + + await this.mutator.createItem(ContentType.VaultListing, FillItemContentSpecialized(content), true) + + for (const contact of trustedContacts) { + if (contact.isMe) { + throw new Error('Should not receive isMe contact from invite') + } + + await this.contacts.createOrEditTrustedContact({ + name: contact.name, + contactUuid: contact.contactUuid, + publicKey: contact.publicKeySet.encryption, + signingPublicKey: contact.publicKeySet.signing, + }) + } + + void this.sync.sync({ sourceDescription: 'Not awaiting due to this event handler running from sync response' }) + } +} diff --git a/packages/services/src/Domain/AsymmetricMessage/UseCase/HandleTrustedSharedVaultRootKeyChangedMessage.ts b/packages/services/src/Domain/AsymmetricMessage/UseCase/HandleTrustedSharedVaultRootKeyChangedMessage.ts new file mode 100644 index 000000000..1bc493b84 --- /dev/null +++ b/packages/services/src/Domain/AsymmetricMessage/UseCase/HandleTrustedSharedVaultRootKeyChangedMessage.ts @@ -0,0 +1,44 @@ +import { ItemManagerInterface } from './../../Item/ItemManagerInterface' +import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface' +import { SyncServiceInterface } from '../../Sync/SyncServiceInterface' +import { + KeySystemRootKeyInterface, + AsymmetricMessageSharedVaultRootKeyChanged, + FillItemContent, + KeySystemRootKeyContent, + VaultListingMutator, +} from '@standardnotes/models' + +import { ContentType } from '@standardnotes/common' +import { GetVaultUseCase } from '../../Vaults/UseCase/GetVault' +import { EncryptionProviderInterface } from '@standardnotes/encryption' + +export class HandleTrustedSharedVaultRootKeyChangedMessage { + constructor( + private mutator: MutatorClientInterface, + private items: ItemManagerInterface, + private sync: SyncServiceInterface, + private encryption: EncryptionProviderInterface, + ) {} + + async execute(message: AsymmetricMessageSharedVaultRootKeyChanged): Promise { + const rootKeyContent = message.data.rootKey + + await this.mutator.createItem( + ContentType.KeySystemRootKey, + FillItemContent(rootKeyContent), + true, + ) + + const vault = new GetVaultUseCase(this.items).execute({ keySystemIdentifier: rootKeyContent.systemIdentifier }) + if (vault) { + await this.mutator.changeItem(vault, (mutator) => { + mutator.rootKeyParams = rootKeyContent.keyParams + }) + } + + await this.encryption.decryptErroredPayloads() + + void this.sync.sync({ sourceDescription: 'Not awaiting due to this event handler running from sync response' }) + } +} diff --git a/packages/services/src/Domain/AsymmetricMessage/UseCase/SendAsymmetricMessageUseCase.ts b/packages/services/src/Domain/AsymmetricMessage/UseCase/SendAsymmetricMessageUseCase.ts new file mode 100644 index 000000000..175903e27 --- /dev/null +++ b/packages/services/src/Domain/AsymmetricMessage/UseCase/SendAsymmetricMessageUseCase.ts @@ -0,0 +1,24 @@ +import { ClientDisplayableError, isErrorResponse, AsymmetricMessageServerHash } from '@standardnotes/responses' +import { AsymmetricMessageServerInterface } from '@standardnotes/api' + +export class SendAsymmetricMessageUseCase { + constructor(private messageServer: AsymmetricMessageServerInterface) {} + + async execute(params: { + recipientUuid: string + encryptedMessage: string + replaceabilityIdentifier: string | undefined + }): Promise { + const response = await this.messageServer.createMessage({ + recipientUuid: params.recipientUuid, + encryptedMessage: params.encryptedMessage, + replaceabilityIdentifier: params.replaceabilityIdentifier, + }) + + if (isErrorResponse(response)) { + return ClientDisplayableError.FromError(response.data.error) + } + + return response.data.message + } +} diff --git a/packages/services/src/Domain/AsymmetricMessage/UseCase/SendOwnContactChangeMessage.ts b/packages/services/src/Domain/AsymmetricMessage/UseCase/SendOwnContactChangeMessage.ts new file mode 100644 index 000000000..3c3fe9efc --- /dev/null +++ b/packages/services/src/Domain/AsymmetricMessage/UseCase/SendOwnContactChangeMessage.ts @@ -0,0 +1,47 @@ +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { AsymmetricMessageServerHash, ClientDisplayableError } from '@standardnotes/responses' +import { + TrustedContactInterface, + AsymmetricMessagePayloadType, + AsymmetricMessageSenderKeypairChanged, +} from '@standardnotes/models' +import { AsymmetricMessageServer } from '@standardnotes/api' +import { PkcKeyPair } from '@standardnotes/sncrypto-common' +import { SendAsymmetricMessageUseCase } from './SendAsymmetricMessageUseCase' + +export class SendOwnContactChangeMessage { + constructor(private encryption: EncryptionProviderInterface, private messageServer: AsymmetricMessageServer) {} + + async execute(params: { + senderOldKeyPair: PkcKeyPair + senderOldSigningKeyPair: PkcKeyPair + senderNewKeyPair: PkcKeyPair + senderNewSigningKeyPair: PkcKeyPair + contact: TrustedContactInterface + }): Promise { + const message: AsymmetricMessageSenderKeypairChanged = { + type: AsymmetricMessagePayloadType.SenderKeypairChanged, + data: { + recipientUuid: params.contact.contactUuid, + newEncryptionPublicKey: params.senderNewKeyPair.publicKey, + newSigningPublicKey: params.senderNewSigningKeyPair.publicKey, + }, + } + + const encryptedMessage = this.encryption.asymmetricallyEncryptMessage({ + message: message, + senderKeyPair: params.senderOldKeyPair, + senderSigningKeyPair: params.senderOldSigningKeyPair, + recipientPublicKey: params.contact.publicKeySet.encryption, + }) + + const sendMessageUseCase = new SendAsymmetricMessageUseCase(this.messageServer) + const sendMessageResult = await sendMessageUseCase.execute({ + recipientUuid: params.contact.contactUuid, + encryptedMessage, + replaceabilityIdentifier: undefined, + }) + + return sendMessageResult + } +} diff --git a/packages/services/src/Domain/Backups/BackupService.spec.ts b/packages/services/src/Domain/Backups/BackupService.spec.ts index adcdbdea1..892742077 100644 --- a/packages/services/src/Domain/Backups/BackupService.spec.ts +++ b/packages/services/src/Domain/Backups/BackupService.spec.ts @@ -32,16 +32,13 @@ describe('backup service', () => { beforeEach(() => { apiService = {} as jest.Mocked apiService.addEventObserver = jest.fn() - apiService.createFileValetToken = jest.fn() + apiService.createUserFileValetToken = jest.fn() apiService.downloadFile = jest.fn() apiService.deleteFile = jest.fn().mockReturnValue({}) itemManager = {} as jest.Mocked - itemManager.createItem = jest.fn() itemManager.createTemplateItem = jest.fn().mockReturnValue({}) - itemManager.setItemToBeDeleted = jest.fn() itemManager.addObserver = jest.fn() - itemManager.changeItem = jest.fn() status = {} as jest.Mocked diff --git a/packages/services/src/Domain/Backups/BackupService.ts b/packages/services/src/Domain/Backups/BackupService.ts index 7e80d84fd..a19695159 100644 --- a/packages/services/src/Domain/Backups/BackupService.ts +++ b/packages/services/src/Domain/Backups/BackupService.ts @@ -515,7 +515,7 @@ export class FilesBackupService extends AbstractService implements BackupService }, }) - const token = await this.api.createFileValetToken(file.remoteIdentifier, 'read') + const token = await this.api.createUserFileValetToken(file.remoteIdentifier, 'read') if (token instanceof ClientDisplayableError) { this.status.removeMessage(messageId) @@ -536,9 +536,11 @@ export class FilesBackupService extends AbstractService implements BackupService const metaFileAsString = JSON.stringify(metaFile, null, 2) + const downloadType = !file.user_uuid || file.user_uuid === this.session.getSureUser().uuid ? 'user' : 'shared-vault' + const result = await this.device.saveFilesBackupsFile(location, file.uuid, metaFileAsString, { chunkSizes: file.encryptedChunkSizes, - url: this.api.getFilesDownloadUrl(), + url: this.api.getFilesDownloadUrl(downloadType), valetToken: token, }) diff --git a/packages/services/src/Domain/Challenge/ChallengeServiceInterface.ts b/packages/services/src/Domain/Challenge/ChallengeServiceInterface.ts index 11aa4b380..10020248e 100644 --- a/packages/services/src/Domain/Challenge/ChallengeServiceInterface.ts +++ b/packages/services/src/Domain/Challenge/ChallengeServiceInterface.ts @@ -1,3 +1,5 @@ +import { ChallengeArtifacts } from './Types/ChallengeArtifacts' +import { ChallengeValue } from './Types/ChallengeValue' import { RootKeyInterface } from '@standardnotes/models' import { AbstractService } from '../Service/AbstractService' @@ -5,6 +7,7 @@ import { ChallengeInterface } from './ChallengeInterface' import { ChallengePromptInterface } from './Prompt/ChallengePromptInterface' import { ChallengeResponseInterface } from './ChallengeResponseInterface' import { ChallengeReason } from './Types/ChallengeReason' +import { ChallengeObserver } from './Types/ChallengeObserver' export interface ChallengeServiceInterface extends AbstractService { /** @@ -20,7 +23,7 @@ export interface ChallengeServiceInterface extends AbstractService { subheading?: string, ): ChallengeInterface completeChallenge(challenge: ChallengeInterface): void - promptForAccountPassword(): Promise + promptForAccountPassword(): Promise getWrappingKeyIfApplicable(passcode?: string): Promise< | { canceled?: undefined @@ -35,4 +38,11 @@ export interface ChallengeServiceInterface extends AbstractService { canceled?: undefined } > + addChallengeObserver(challenge: ChallengeInterface, observer: ChallengeObserver): () => void + setValidationStatusForChallenge( + challenge: ChallengeInterface, + value: ChallengeValue, + valid: boolean, + artifacts?: ChallengeArtifacts, + ): void } diff --git a/packages/services/src/Domain/Challenge/Types/ChallengeObserver.ts b/packages/services/src/Domain/Challenge/Types/ChallengeObserver.ts new file mode 100644 index 000000000..3390180ce --- /dev/null +++ b/packages/services/src/Domain/Challenge/Types/ChallengeObserver.ts @@ -0,0 +1,10 @@ +import { ChallengeResponseInterface } from '../ChallengeResponseInterface' +import { ChallengeValueCallback } from './ChallengeValueCallback' + +export type ChallengeObserver = { + onValidValue?: ChallengeValueCallback + onInvalidValue?: ChallengeValueCallback + onNonvalidatedSubmit?: (response: ChallengeResponseInterface) => void + onComplete?: (response: ChallengeResponseInterface) => void + onCancel?: () => void +} diff --git a/packages/services/src/Domain/Challenge/Types/ChallengeValueCallback.ts b/packages/services/src/Domain/Challenge/Types/ChallengeValueCallback.ts new file mode 100644 index 000000000..d6bd685f3 --- /dev/null +++ b/packages/services/src/Domain/Challenge/Types/ChallengeValueCallback.ts @@ -0,0 +1,3 @@ +import { ChallengeValue } from './ChallengeValue' + +export type ChallengeValueCallback = (value: ChallengeValue) => void diff --git a/packages/services/src/Domain/Challenge/index.ts b/packages/services/src/Domain/Challenge/index.ts index 20cf444bc..e5e200006 100644 --- a/packages/services/src/Domain/Challenge/index.ts +++ b/packages/services/src/Domain/Challenge/index.ts @@ -11,3 +11,5 @@ export * from './Types/ChallengeRawValue' export * from './Types/ChallengeReason' export * from './Types/ChallengeValidation' export * from './Types/ChallengeValue' +export * from './Types/ChallengeObserver' +export * from './Types/ChallengeValueCallback' diff --git a/packages/services/src/Domain/Component/ComponentManagerInterface.ts b/packages/services/src/Domain/Component/ComponentManagerInterface.ts index ad19279a8..8ae47a091 100644 --- a/packages/services/src/Domain/Component/ComponentManagerInterface.ts +++ b/packages/services/src/Domain/Component/ComponentManagerInterface.ts @@ -21,4 +21,6 @@ export interface ComponentManagerInterface { presentPermissionsDialog(_dialog: PermissionDialog): void legacyGetDefaultEditor(): SNComponent | undefined componentWithIdentifier(identifier: FeatureIdentifier | string): SNComponent | undefined + toggleTheme(uuid: string): Promise + toggleComponent(uuid: string): Promise } diff --git a/packages/services/src/Domain/Contacts/CollaborationID.ts b/packages/services/src/Domain/Contacts/CollaborationID.ts new file mode 100644 index 000000000..580344c38 --- /dev/null +++ b/packages/services/src/Domain/Contacts/CollaborationID.ts @@ -0,0 +1,8 @@ +export const Version1CollaborationId = '1' + +export type CollaborationIDData = { + version: string + userUuid: string + publicKey: string + signingPublicKey: string +} diff --git a/packages/services/src/Domain/Contacts/ContactService.ts b/packages/services/src/Domain/Contacts/ContactService.ts new file mode 100644 index 000000000..0d139583a --- /dev/null +++ b/packages/services/src/Domain/Contacts/ContactService.ts @@ -0,0 +1,264 @@ +import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' +import { ApplicationStage } from './../Application/ApplicationStage' +import { SingletonManagerInterface } from './../Singleton/SingletonManagerInterface' +import { UserKeyPairChangedEventData } from './../Session/UserKeyPairChangedEventData' +import { SessionEvent } from './../Session/SessionEvent' +import { InternalEventInterface } from './../Internal/InternalEventInterface' +import { InternalEventHandlerInterface } from './../Internal/InternalEventHandlerInterface' +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { SharedVaultInviteServerHash, SharedVaultUserServerHash } from '@standardnotes/responses' +import { + TrustedContactContent, + TrustedContactContentSpecialized, + TrustedContactInterface, + FillItemContent, + TrustedContactMutator, + DecryptedItemInterface, +} from '@standardnotes/models' +import { ContentType } from '@standardnotes/common' +import { AbstractService } from '../Service/AbstractService' +import { SyncServiceInterface } from '../Sync/SyncServiceInterface' +import { ItemManagerInterface } from '../Item/ItemManagerInterface' +import { SessionsClientInterface } from '../Session/SessionsClientInterface' +import { ContactServiceEvent, ContactServiceInterface } from '../Contacts/ContactServiceInterface' +import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' +import { UserClientInterface } from '../User/UserClientInterface' +import { CollaborationIDData, Version1CollaborationId } from './CollaborationID' +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { ValidateItemSignerUseCase } from './UseCase/ValidateItemSigner' +import { ValidateItemSignerResult } from './UseCase/ValidateItemSignerResult' +import { FindTrustedContactUseCase } from './UseCase/FindTrustedContact' +import { SelfContactManager } from './Managers/SelfContactManager' +import { CreateOrEditTrustedContactUseCase } from './UseCase/CreateOrEditTrustedContact' +import { UpdateTrustedContactUseCase } from './UseCase/UpdateTrustedContact' + +export class ContactService + extends AbstractService + implements ContactServiceInterface, InternalEventHandlerInterface +{ + private selfContactManager: SelfContactManager + + constructor( + private sync: SyncServiceInterface, + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private session: SessionsClientInterface, + private crypto: PureCryptoInterface, + private user: UserClientInterface, + private encryption: EncryptionProviderInterface, + singletons: SingletonManagerInterface, + eventBus: InternalEventBusInterface, + ) { + super(eventBus) + + this.selfContactManager = new SelfContactManager(sync, items, mutator, session, singletons) + + eventBus.addEventHandler(this, SessionEvent.UserKeyPairChanged) + } + + public override async handleApplicationStage(stage: ApplicationStage): Promise { + await super.handleApplicationStage(stage) + await this.selfContactManager.handleApplicationStage(stage) + } + + async handleEvent(event: InternalEventInterface): Promise { + if (event.type === SessionEvent.UserKeyPairChanged) { + const data = event.payload as UserKeyPairChangedEventData + + await this.selfContactManager.updateWithNewPublicKeySet({ + encryption: data.newKeyPair.publicKey, + signing: data.newSigningKeyPair.publicKey, + }) + } + } + + private get userUuid(): string { + return this.session.getSureUser().uuid + } + + getSelfContact(): TrustedContactInterface | undefined { + return this.selfContactManager.selfContact + } + + public isCollaborationEnabled(): boolean { + return !this.session.isUserMissingKeyPair() + } + + public async enableCollaboration(): Promise { + await this.user.updateAccountWithFirstTimeKeyPair() + } + + public getCollaborationID(): string { + const publicKey = this.session.getPublicKey() + if (!publicKey) { + throw new Error('Collaboration not enabled') + } + + return this.buildCollaborationId({ + version: Version1CollaborationId, + userUuid: this.session.getSureUser().uuid, + publicKey, + signingPublicKey: this.session.getSigningPublicKey(), + }) + } + + private buildCollaborationId(params: CollaborationIDData): string { + const string = `${params.version}:${params.userUuid}:${params.publicKey}:${params.signingPublicKey}` + return this.crypto.base64Encode(string) + } + + public parseCollaborationID(collaborationID: string): CollaborationIDData { + const decoded = this.crypto.base64Decode(collaborationID) + const [version, userUuid, publicKey, signingPublicKey] = decoded.split(':') + return { version, userUuid, publicKey, signingPublicKey } + } + + public getCollaborationIDFromInvite(invite: SharedVaultInviteServerHash): string { + const publicKeySet = this.encryption.getSenderPublicKeySetFromAsymmetricallyEncryptedString( + invite.encrypted_message, + ) + return this.buildCollaborationId({ + version: Version1CollaborationId, + userUuid: invite.sender_uuid, + publicKey: publicKeySet.encryption, + signingPublicKey: publicKeySet.signing, + }) + } + + public addTrustedContactFromCollaborationID( + collaborationID: string, + name?: string, + ): Promise { + const { userUuid, publicKey, signingPublicKey } = this.parseCollaborationID(collaborationID) + return this.createOrEditTrustedContact({ + name: name ?? '', + contactUuid: userUuid, + publicKey, + signingPublicKey, + }) + } + + async editTrustedContactFromCollaborationID( + contact: TrustedContactInterface, + params: { name: string; collaborationID: string }, + ): Promise { + const { publicKey, signingPublicKey, userUuid } = this.parseCollaborationID(params.collaborationID) + if (userUuid !== contact.contactUuid) { + throw new Error("Collaboration ID's user uuid does not match contact UUID") + } + + const updatedContact = await this.mutator.changeItem( + contact, + (mutator) => { + mutator.name = params.name + + if (publicKey !== contact.publicKeySet.encryption || signingPublicKey !== contact.publicKeySet.signing) { + mutator.addPublicKey({ + encryption: publicKey, + signing: signingPublicKey, + }) + } + }, + ) + + await this.sync.sync() + + return updatedContact + } + + async updateTrustedContact( + contact: TrustedContactInterface, + params: { name: string; publicKey: string; signingPublicKey: string }, + ): Promise { + const usecase = new UpdateTrustedContactUseCase(this.mutator, this.sync) + const updatedContact = await usecase.execute(contact, params) + + return updatedContact + } + + async createOrUpdateTrustedContactFromContactShare( + data: TrustedContactContentSpecialized, + ): Promise { + if (data.contactUuid === this.userUuid) { + throw new Error('Cannot receive self from contact share') + } + + let contact = this.findTrustedContact(data.contactUuid) + if (contact) { + contact = await this.mutator.changeItem(contact, (mutator) => { + mutator.name = data.name + mutator.replacePublicKeySet(data.publicKeySet) + }) + } else { + contact = await this.mutator.createItem( + ContentType.TrustedContact, + FillItemContent(data), + true, + ) + } + + await this.sync.sync() + + return contact + } + + async createOrEditTrustedContact(params: { + name?: string + contactUuid: string + publicKey: string + signingPublicKey: string + isMe?: boolean + }): Promise { + const usecase = new CreateOrEditTrustedContactUseCase(this.items, this.mutator, this.sync) + const contact = await usecase.execute(params) + return contact + } + + async deleteContact(contact: TrustedContactInterface): Promise { + if (contact.isMe) { + throw new Error('Cannot delete self') + } + + await this.mutator.setItemToBeDeleted(contact) + await this.sync.sync() + } + + getAllContacts(): TrustedContactInterface[] { + return this.items.getItems(ContentType.TrustedContact) + } + + findTrustedContact(userUuid: string): TrustedContactInterface | undefined { + const usecase = new FindTrustedContactUseCase(this.items) + return usecase.execute({ userUuid }) + } + + findTrustedContactForServerUser(user: SharedVaultUserServerHash): TrustedContactInterface | undefined { + return this.findTrustedContact(user.user_uuid) + } + + findTrustedContactForInvite(invite: SharedVaultInviteServerHash): TrustedContactInterface | undefined { + return this.findTrustedContact(invite.user_uuid) + } + + getCollaborationIDForTrustedContact(contact: TrustedContactInterface): string { + return this.buildCollaborationId({ + version: Version1CollaborationId, + userUuid: contact.content.contactUuid, + publicKey: contact.content.publicKeySet.encryption, + signingPublicKey: contact.content.publicKeySet.signing, + }) + } + + isItemAuthenticallySigned(item: DecryptedItemInterface): ValidateItemSignerResult { + const usecase = new ValidateItemSignerUseCase(this.items) + return usecase.execute(item) + } + + override deinit(): void { + super.deinit() + this.selfContactManager.deinit() + ;(this.sync as unknown) = undefined + ;(this.items as unknown) = undefined + ;(this.selfContactManager as unknown) = undefined + } +} diff --git a/packages/services/src/Domain/Contacts/ContactServiceInterface.ts b/packages/services/src/Domain/Contacts/ContactServiceInterface.ts new file mode 100644 index 000000000..6a24825bf --- /dev/null +++ b/packages/services/src/Domain/Contacts/ContactServiceInterface.ts @@ -0,0 +1,43 @@ +import { + DecryptedItemInterface, + TrustedContactContentSpecialized, + TrustedContactInterface, +} from '@standardnotes/models' +import { AbstractService } from '../Service/AbstractService' +import { SharedVaultInviteServerHash, SharedVaultUserServerHash } from '@standardnotes/responses' +import { ValidateItemSignerResult } from './UseCase/ValidateItemSignerResult' + +export enum ContactServiceEvent {} + +export interface ContactServiceInterface extends AbstractService { + isCollaborationEnabled(): boolean + enableCollaboration(): Promise + getCollaborationID(): string + getCollaborationIDFromInvite(invite: SharedVaultInviteServerHash): string + addTrustedContactFromCollaborationID( + collaborationID: string, + name?: string, + ): Promise + getCollaborationIDForTrustedContact(contact: TrustedContactInterface): string + + createOrEditTrustedContact(params: { + contactUuid: string + name?: string + publicKey: string + signingPublicKey: string + }): Promise + createOrUpdateTrustedContactFromContactShare(data: TrustedContactContentSpecialized): Promise + editTrustedContactFromCollaborationID( + contact: TrustedContactInterface, + params: { name: string; collaborationID: string }, + ): Promise + deleteContact(contact: TrustedContactInterface): Promise + + getAllContacts(): TrustedContactInterface[] + getSelfContact(): TrustedContactInterface | undefined + findTrustedContact(userUuid: string): TrustedContactInterface | undefined + findTrustedContactForServerUser(user: SharedVaultUserServerHash): TrustedContactInterface | undefined + findTrustedContactForInvite(invite: SharedVaultInviteServerHash): TrustedContactInterface | undefined + + isItemAuthenticallySigned(item: DecryptedItemInterface): ValidateItemSignerResult +} diff --git a/packages/services/src/Domain/Contacts/Managers/SelfContactManager.ts b/packages/services/src/Domain/Contacts/Managers/SelfContactManager.ts new file mode 100644 index 000000000..ace8c11fe --- /dev/null +++ b/packages/services/src/Domain/Contacts/Managers/SelfContactManager.ts @@ -0,0 +1,129 @@ +import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface' +import { InternalFeature } from './../../InternalFeatures/InternalFeature' +import { InternalFeatureService } from '../../InternalFeatures/InternalFeatureService' +import { ApplicationStage } from './../../Application/ApplicationStage' +import { SingletonManagerInterface } from './../../Singleton/SingletonManagerInterface' +import { SyncEvent } from './../../Event/SyncEvent' +import { SessionsClientInterface } from '../../Session/SessionsClientInterface' +import { ItemManagerInterface } from '../../Item/ItemManagerInterface' +import { SyncServiceInterface } from '../../Sync/SyncServiceInterface' +import { + ContactPublicKeySet, + FillItemContent, + TrustedContact, + TrustedContactContent, + TrustedContactContentSpecialized, + TrustedContactInterface, +} from '@standardnotes/models' +import { ContentType } from '@standardnotes/common' +import { CreateOrEditTrustedContactUseCase } from '../UseCase/CreateOrEditTrustedContact' +import { PublicKeySet } from '@standardnotes/encryption' + +export class SelfContactManager { + public selfContact?: TrustedContactInterface + private shouldReloadSelfContact = true + private isReloadingSelfContact = false + private eventDisposers: (() => void)[] = [] + + constructor( + private sync: SyncServiceInterface, + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private session: SessionsClientInterface, + private singletons: SingletonManagerInterface, + ) { + this.eventDisposers.push( + items.addObserver(ContentType.TrustedContact, () => { + this.shouldReloadSelfContact = true + }), + ) + + this.eventDisposers.push( + sync.addEventObserver((event) => { + if (event === SyncEvent.SyncCompletedWithAllItemsUploaded || event === SyncEvent.LocalDataIncrementalLoad) { + void this.reloadSelfContact() + } + }), + ) + } + + public async handleApplicationStage(stage: ApplicationStage): Promise { + if (stage === ApplicationStage.LoadedDatabase_12) { + this.selfContact = this.singletons.findSingleton( + ContentType.UserPrefs, + TrustedContact.singletonPredicate, + ) + } + } + + public async updateWithNewPublicKeySet(publicKeySet: PublicKeySet) { + if (!InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) { + return + } + + if (!this.selfContact) { + return + } + + const usecase = new CreateOrEditTrustedContactUseCase(this.items, this.mutator, this.sync) + await usecase.execute({ + name: 'Me', + contactUuid: this.selfContact.contactUuid, + publicKey: publicKeySet.encryption, + signingPublicKey: publicKeySet.signing, + }) + } + + private async reloadSelfContact() { + if (!InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) { + return + } + + if (!this.shouldReloadSelfContact || this.isReloadingSelfContact) { + return + } + + if (!this.session.isSignedIn()) { + return + } + + if (this.session.isUserMissingKeyPair()) { + return + } + + this.isReloadingSelfContact = true + + const content: TrustedContactContentSpecialized = { + name: 'Me', + isMe: true, + contactUuid: this.session.getSureUser().uuid, + publicKeySet: ContactPublicKeySet.FromJson({ + encryption: this.session.getPublicKey(), + signing: this.session.getSigningPublicKey(), + isRevoked: false, + timestamp: new Date(), + }), + } + + try { + this.selfContact = await this.singletons.findOrCreateSingleton( + TrustedContact.singletonPredicate, + ContentType.TrustedContact, + FillItemContent(content), + ) + + this.shouldReloadSelfContact = false + } finally { + this.isReloadingSelfContact = false + } + } + + deinit() { + this.eventDisposers.forEach((disposer) => disposer()) + ;(this.sync as unknown) = undefined + ;(this.items as unknown) = undefined + ;(this.mutator as unknown) = undefined + ;(this.session as unknown) = undefined + ;(this.singletons as unknown) = undefined + } +} diff --git a/packages/services/src/Domain/Contacts/UnknownContactName.ts b/packages/services/src/Domain/Contacts/UnknownContactName.ts new file mode 100644 index 000000000..5e112bf51 --- /dev/null +++ b/packages/services/src/Domain/Contacts/UnknownContactName.ts @@ -0,0 +1 @@ +export const UnknownContactName = 'Unnamed contact' diff --git a/packages/services/src/Domain/Contacts/UseCase/CreateOrEditTrustedContact.ts b/packages/services/src/Domain/Contacts/UseCase/CreateOrEditTrustedContact.ts new file mode 100644 index 000000000..27940adba --- /dev/null +++ b/packages/services/src/Domain/Contacts/UseCase/CreateOrEditTrustedContact.ts @@ -0,0 +1,61 @@ +import { SyncServiceInterface } from './../../Sync/SyncServiceInterface' +import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface' +import { ItemManagerInterface } from './../../Item/ItemManagerInterface' +import { + ContactPublicKeySet, + FillItemContent, + TrustedContactContent, + TrustedContactContentSpecialized, + TrustedContactInterface, +} from '@standardnotes/models' +import { FindTrustedContactUseCase } from './FindTrustedContact' +import { UnknownContactName } from '../UnknownContactName' +import { ContentType } from '@standardnotes/common' +import { UpdateTrustedContactUseCase } from './UpdateTrustedContact' + +export class CreateOrEditTrustedContactUseCase { + constructor( + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private sync: SyncServiceInterface, + ) {} + + async execute(params: { + name?: string + contactUuid: string + publicKey: string + signingPublicKey: string + isMe?: boolean + }): Promise { + const findUsecase = new FindTrustedContactUseCase(this.items) + const existingContact = findUsecase.execute({ userUuid: params.contactUuid }) + + if (existingContact) { + const updateUsecase = new UpdateTrustedContactUseCase(this.mutator, this.sync) + await updateUsecase.execute(existingContact, { ...params, name: params.name ?? existingContact.name }) + return existingContact + } + + const content: TrustedContactContentSpecialized = { + name: params.name ?? UnknownContactName, + publicKeySet: ContactPublicKeySet.FromJson({ + encryption: params.publicKey, + signing: params.signingPublicKey, + isRevoked: false, + timestamp: new Date(), + }), + contactUuid: params.contactUuid, + isMe: params.isMe ?? false, + } + + const contact = await this.mutator.createItem( + ContentType.TrustedContact, + FillItemContent(content), + true, + ) + + await this.sync.sync() + + return contact + } +} diff --git a/packages/services/src/Domain/Contacts/UseCase/FindContactQuery.ts b/packages/services/src/Domain/Contacts/UseCase/FindContactQuery.ts new file mode 100644 index 000000000..31a2bf652 --- /dev/null +++ b/packages/services/src/Domain/Contacts/UseCase/FindContactQuery.ts @@ -0,0 +1 @@ +export type FindContactQuery = { userUuid: string } | { signingPublicKey: string } | { publicKey: string } diff --git a/packages/services/src/Domain/Contacts/UseCase/FindTrustedContact.ts b/packages/services/src/Domain/Contacts/UseCase/FindTrustedContact.ts new file mode 100644 index 000000000..4240fd852 --- /dev/null +++ b/packages/services/src/Domain/Contacts/UseCase/FindTrustedContact.ts @@ -0,0 +1,29 @@ +import { Predicate, TrustedContactInterface } from '@standardnotes/models' +import { ItemManagerInterface } from './../../Item/ItemManagerInterface' +import { ContentType } from '@standardnotes/common' +import { FindContactQuery } from './FindContactQuery' + +export class FindTrustedContactUseCase { + constructor(private items: ItemManagerInterface) {} + + execute(query: FindContactQuery): TrustedContactInterface | undefined { + if ('userUuid' in query && query.userUuid) { + return this.items.itemsMatchingPredicate( + ContentType.TrustedContact, + new Predicate('contactUuid', '=', query.userUuid), + )[0] + } + + if ('signingPublicKey' in query && query.signingPublicKey) { + const allContacts = this.items.getItems(ContentType.TrustedContact) + return allContacts.find((contact) => contact.isSigningKeyTrusted(query.signingPublicKey)) + } + + if ('publicKey' in query && query.publicKey) { + const allContacts = this.items.getItems(ContentType.TrustedContact) + return allContacts.find((contact) => contact.isPublicKeyTrusted(query.publicKey)) + } + + throw new Error('Invalid query') + } +} diff --git a/packages/services/src/Domain/Contacts/UseCase/UpdateTrustedContact.ts b/packages/services/src/Domain/Contacts/UseCase/UpdateTrustedContact.ts new file mode 100644 index 000000000..a071b1bba --- /dev/null +++ b/packages/services/src/Domain/Contacts/UseCase/UpdateTrustedContact.ts @@ -0,0 +1,32 @@ +import { SyncServiceInterface } from './../../Sync/SyncServiceInterface' +import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface' +import { TrustedContactInterface, TrustedContactMutator } from '@standardnotes/models' + +export class UpdateTrustedContactUseCase { + constructor(private mutator: MutatorClientInterface, private sync: SyncServiceInterface) {} + + async execute( + contact: TrustedContactInterface, + params: { name: string; publicKey: string; signingPublicKey: string }, + ): Promise { + const updatedContact = await this.mutator.changeItem( + contact, + (mutator) => { + mutator.name = params.name + if ( + params.publicKey !== contact.publicKeySet.encryption || + params.signingPublicKey !== contact.publicKeySet.signing + ) { + mutator.addPublicKey({ + encryption: params.publicKey, + signing: params.signingPublicKey, + }) + } + }, + ) + + await this.sync.sync() + + return updatedContact + } +} diff --git a/packages/services/src/Domain/Contacts/UseCase/ValidateItemSigner.spec.ts b/packages/services/src/Domain/Contacts/UseCase/ValidateItemSigner.spec.ts new file mode 100644 index 000000000..6c6c5aa5a --- /dev/null +++ b/packages/services/src/Domain/Contacts/UseCase/ValidateItemSigner.spec.ts @@ -0,0 +1,347 @@ +import { + DecryptedItemInterface, + PayloadSource, + PersistentSignatureData, + TrustedContactInterface, +} from '@standardnotes/models' +import { ItemManagerInterface } from './../../Item/ItemManagerInterface' +import { ValidateItemSignerUseCase } from './ValidateItemSigner' + +describe('validate item signer use case', () => { + let usecase: ValidateItemSignerUseCase + let items: ItemManagerInterface + + const trustedContact = {} as jest.Mocked + trustedContact.isSigningKeyTrusted = jest.fn().mockReturnValue(true) + + beforeEach(() => { + items = {} as jest.Mocked + usecase = new ValidateItemSignerUseCase(items) + }) + + const createItem = (params: { + last_edited_by_uuid: string | undefined + shared_vault_uuid: string | undefined + signatureData: PersistentSignatureData | undefined + source?: PayloadSource + }): jest.Mocked => { + const payload = { + source: params.source ?? PayloadSource.RemoteRetrieved, + } as jest.Mocked + + const item = { + last_edited_by_uuid: params.last_edited_by_uuid, + shared_vault_uuid: params.shared_vault_uuid, + signatureData: params.signatureData, + payload: payload, + } as unknown as jest.Mocked + + return item + } + + describe('has last edited by uuid', () => { + describe('trusted contact not found', () => { + beforeEach(() => { + items.itemsMatchingPredicate = jest.fn().mockReturnValue([]) + }) + + it('should return invalid signing is required', () => { + const item = createItem({ + last_edited_by_uuid: 'uuid-123', + shared_vault_uuid: 'shared-vault-123', + signatureData: undefined, + }) + + const result = usecase.execute(item) + expect(result).toEqual('no') + }) + + it('should return not applicable signing is not required', () => { + const item = createItem({ + last_edited_by_uuid: 'uuid-123', + shared_vault_uuid: undefined, + signatureData: undefined, + }) + + const result = usecase.execute(item) + expect(result).toEqual('not-applicable') + }) + }) + + describe('trusted contact found for last editor', () => { + beforeEach(() => { + items.itemsMatchingPredicate = jest.fn().mockReturnValue([trustedContact]) + }) + + describe('does not have signature data', () => { + it('should return not applicable if the item was just recently created', () => { + const item = createItem({ + last_edited_by_uuid: 'uuid-123', + shared_vault_uuid: 'shared-vault-123', + signatureData: undefined, + source: PayloadSource.Constructor, + }) + + const result = usecase.execute(item) + expect(result).toEqual('not-applicable') + }) + + it('should return not applicable if the item was just recently saved', () => { + const item = createItem({ + last_edited_by_uuid: 'uuid-123', + shared_vault_uuid: 'shared-vault-123', + signatureData: undefined, + source: PayloadSource.RemoteSaved, + }) + + const result = usecase.execute(item) + expect(result).toEqual('not-applicable') + }) + + it('should return invalid if signing is required', () => { + const item = createItem({ + last_edited_by_uuid: 'uuid-123', + shared_vault_uuid: 'shared-vault-123', + signatureData: undefined, + }) + + const result = usecase.execute(item) + expect(result).toEqual('no') + }) + + it('should return not applicable if signing is not required', () => { + const item = createItem({ + last_edited_by_uuid: 'uuid-123', + signatureData: undefined, + shared_vault_uuid: undefined, + }) + + const result = usecase.execute(item) + expect(result).toEqual('not-applicable') + }) + }) + + describe('has signature data', () => { + describe('signature data does not have result', () => { + it('should return invalid if signing is required', () => { + const item = createItem({ + last_edited_by_uuid: 'uuid-123', + shared_vault_uuid: 'shared-vault-123', + signatureData: { + required: true, + } as jest.Mocked, + }) + + const result = usecase.execute(item) + expect(result).toEqual('no') + }) + + it('should return not applicable if signing is not required', () => { + const item = createItem({ + last_edited_by_uuid: 'uuid-123', + shared_vault_uuid: undefined, + signatureData: { + required: false, + } as jest.Mocked, + }) + + const result = usecase.execute(item) + expect(result).toEqual('not-applicable') + }) + }) + + describe('signature data has result', () => { + it('should return invalid if signature result does not pass', () => { + const item = createItem({ + last_edited_by_uuid: 'uuid-123', + shared_vault_uuid: 'shared-vault-123', + signatureData: { + required: true, + result: { + passes: false, + }, + } as jest.Mocked, + }) + + const result = usecase.execute(item) + expect(result).toEqual('no') + }) + + it('should return invalid if signature result passes and a trusted contact is NOT found for signature public key', () => { + const item = createItem({ + last_edited_by_uuid: 'uuid-123', + shared_vault_uuid: 'shared-vault-123', + signatureData: { + required: true, + result: { + passes: true, + publicKey: 'pk-123', + }, + } as jest.Mocked, + }) + + items.itemsMatchingPredicate = jest.fn().mockReturnValue([]) + + const result = usecase.execute(item) + expect(result).toEqual('no') + }) + + it('should return valid if signature result passes and a trusted contact is found for signature public key', () => { + const item = createItem({ + last_edited_by_uuid: 'uuid-123', + shared_vault_uuid: 'shared-vault-123', + signatureData: { + required: true, + result: { + passes: true, + publicKey: 'pk-123', + }, + } as jest.Mocked, + }) + + items.itemsMatchingPredicate = jest.fn().mockReturnValue([trustedContact]) + + const result = usecase.execute(item) + expect(result).toEqual('yes') + }) + }) + }) + }) + }) + + describe('has no last edited by uuid', () => { + describe('does not have signature data', () => { + it('should return not applicable if the item was just recently created', () => { + const item = createItem({ + last_edited_by_uuid: undefined, + shared_vault_uuid: 'shared-vault-123', + signatureData: undefined, + source: PayloadSource.Constructor, + }) + + const result = usecase.execute(item) + expect(result).toEqual('not-applicable') + }) + + it('should return not applicable if the item was just recently saved', () => { + const item = createItem({ + last_edited_by_uuid: undefined, + shared_vault_uuid: 'shared-vault-123', + signatureData: undefined, + source: PayloadSource.RemoteSaved, + }) + + const result = usecase.execute(item) + expect(result).toEqual('not-applicable') + }) + + it('should return invalid if signing is required', () => { + const item = createItem({ + last_edited_by_uuid: undefined, + shared_vault_uuid: 'shared-vault-123', + signatureData: undefined, + }) + + const result = usecase.execute(item) + expect(result).toEqual('no') + }) + + it('should return not applicable if signing is not required', () => { + const item = createItem({ + last_edited_by_uuid: undefined, + shared_vault_uuid: undefined, + signatureData: undefined, + }) + + const result = usecase.execute(item) + expect(result).toEqual('not-applicable') + }) + }) + + describe('has signature data', () => { + describe('signature data does not have result', () => { + it('should return invalid if signing is required', () => { + const item = createItem({ + last_edited_by_uuid: undefined, + shared_vault_uuid: 'shared-vault-123', + signatureData: { + required: true, + } as jest.Mocked, + }) + + const result = usecase.execute(item) + expect(result).toEqual('no') + }) + + it('should return not applicable if signing is not required', () => { + const item = createItem({ + last_edited_by_uuid: undefined, + shared_vault_uuid: undefined, + signatureData: { + required: false, + } as jest.Mocked, + }) + + const result = usecase.execute(item) + expect(result).toEqual('not-applicable') + }) + }) + + describe('signature data has result', () => { + it('should return invalid if signature result does not pass', () => { + const item = createItem({ + last_edited_by_uuid: undefined, + shared_vault_uuid: 'shared-vault-123', + signatureData: { + required: true, + result: { + passes: false, + }, + } as jest.Mocked, + }) + + const result = usecase.execute(item) + expect(result).toEqual('no') + }) + + it('should return invalid if signature result passes and a trusted contact is NOT found for signature public key', () => { + const item = createItem({ + last_edited_by_uuid: undefined, + shared_vault_uuid: 'shared-vault-123', + signatureData: { + required: true, + result: { + passes: true, + publicKey: 'pk-123', + }, + } as jest.Mocked, + }) + + items.getItems = jest.fn().mockReturnValue([]) + + const result = usecase.execute(item) + expect(result).toEqual('no') + }) + + it('should return valid if signature result passes and a trusted contact is found for signature public key', () => { + const item = createItem({ + last_edited_by_uuid: undefined, + shared_vault_uuid: 'shared-vault-123', + signatureData: { + required: true, + result: { + passes: true, + publicKey: 'pk-123', + }, + } as jest.Mocked, + }) + + items.getItems = jest.fn().mockReturnValue([trustedContact]) + + const result = usecase.execute(item) + expect(result).toEqual('yes') + }) + }) + }) + }) +}) diff --git a/packages/services/src/Domain/Contacts/UseCase/ValidateItemSigner.ts b/packages/services/src/Domain/Contacts/UseCase/ValidateItemSigner.ts new file mode 100644 index 000000000..3d5449d1f --- /dev/null +++ b/packages/services/src/Domain/Contacts/UseCase/ValidateItemSigner.ts @@ -0,0 +1,122 @@ +import { ItemManagerInterface } from './../../Item/ItemManagerInterface' +import { doesPayloadRequireSigning } from '@standardnotes/encryption/src/Domain/Operator/004/V004AlgorithmHelpers' +import { DecryptedItemInterface, PayloadSource } from '@standardnotes/models' +import { ValidateItemSignerResult } from './ValidateItemSignerResult' +import { FindTrustedContactUseCase } from './FindTrustedContact' + +export class ValidateItemSignerUseCase { + private findContactUseCase = new FindTrustedContactUseCase(this.items) + + constructor(private items: ItemManagerInterface) {} + + execute(item: DecryptedItemInterface): ValidateItemSignerResult { + const uuidOfLastEditor = item.last_edited_by_uuid + if (uuidOfLastEditor) { + return this.validateSignatureWithLastEditedByUuid(item, uuidOfLastEditor) + } else { + return this.validateSignatureWithNoLastEditedByUuid(item) + } + } + + private isItemLocallyCreatedAndDoesNotRequireSignature(item: DecryptedItemInterface): boolean { + return item.payload.source === PayloadSource.Constructor + } + + private isItemResutOfRemoteSaveAndDoesNotRequireSignature(item: DecryptedItemInterface): boolean { + return item.payload.source === PayloadSource.RemoteSaved + } + + private validateSignatureWithLastEditedByUuid( + item: DecryptedItemInterface, + uuidOfLastEditor: string, + ): ValidateItemSignerResult { + const requiresSignature = doesPayloadRequireSigning(item) + + const trustedContact = this.findContactUseCase.execute({ userUuid: uuidOfLastEditor }) + if (!trustedContact) { + if (requiresSignature) { + return 'no' + } else { + return 'not-applicable' + } + } + + if (!item.signatureData) { + if ( + this.isItemLocallyCreatedAndDoesNotRequireSignature(item) || + this.isItemResutOfRemoteSaveAndDoesNotRequireSignature(item) + ) { + return 'not-applicable' + } + if (requiresSignature) { + return 'no' + } + return 'not-applicable' + } + + const signatureData = item.signatureData + if (!signatureData.result) { + if (signatureData.required) { + return 'no' + } + return 'not-applicable' + } + + const signatureResult = signatureData.result + + if (!signatureResult.passes) { + return 'no' + } + + const signerPublicKey = signatureResult.publicKey + + if (trustedContact.isSigningKeyTrusted(signerPublicKey)) { + return 'yes' + } + + return 'no' + } + + private validateSignatureWithNoLastEditedByUuid(item: DecryptedItemInterface): ValidateItemSignerResult { + const requiresSignature = doesPayloadRequireSigning(item) + + if (!item.signatureData) { + if ( + this.isItemLocallyCreatedAndDoesNotRequireSignature(item) || + this.isItemResutOfRemoteSaveAndDoesNotRequireSignature(item) + ) { + return 'not-applicable' + } + + if (requiresSignature) { + return 'no' + } + + return 'not-applicable' + } + + const signatureData = item.signatureData + if (!signatureData.result) { + if (signatureData.required) { + return 'no' + } + return 'not-applicable' + } + + const signatureResult = signatureData.result + + if (!signatureResult.passes) { + return 'no' + } + + const signerPublicKey = signatureResult.publicKey + + const trustedContact = this.findContactUseCase.execute({ signingPublicKey: signerPublicKey }) + + if (trustedContact) { + return 'yes' + } + + return 'no' + } +} diff --git a/packages/services/src/Domain/Contacts/UseCase/ValidateItemSignerResult.ts b/packages/services/src/Domain/Contacts/UseCase/ValidateItemSignerResult.ts new file mode 100644 index 000000000..6d28bd992 --- /dev/null +++ b/packages/services/src/Domain/Contacts/UseCase/ValidateItemSignerResult.ts @@ -0,0 +1 @@ +export type ValidateItemSignerResult = 'not-applicable' | 'yes' | 'no' diff --git a/packages/services/src/Domain/Device/DatabaseLoadOptions.ts b/packages/services/src/Domain/Device/DatabaseLoadOptions.ts index daad481b8..cebd73e11 100644 --- a/packages/services/src/Domain/Device/DatabaseLoadOptions.ts +++ b/packages/services/src/Domain/Device/DatabaseLoadOptions.ts @@ -18,6 +18,8 @@ export function isChunkFullEntry( export type DatabaseKeysLoadChunkResponse = { keys: { itemsKeys: DatabaseKeysLoadChunk + keySystemRootKeys: DatabaseKeysLoadChunk + keySystemItemsKeys: DatabaseKeysLoadChunk remainingChunks: DatabaseKeysLoadChunk[] } remainingChunksItemCount: number @@ -26,6 +28,8 @@ export type DatabaseKeysLoadChunkResponse = { export type DatabaseFullEntryLoadChunkResponse = { fullEntries: { itemsKeys: DatabaseFullEntryLoadChunk + keySystemRootKeys: DatabaseFullEntryLoadChunk + keySystemItemsKeys: DatabaseFullEntryLoadChunk remainingChunks: DatabaseFullEntryLoadChunk[] } remainingChunksItemCount: number diff --git a/packages/services/src/Domain/Device/DatabaseLoadSorter.ts b/packages/services/src/Domain/Device/DatabaseLoadSorter.ts index 6aa874680..dfdede807 100644 --- a/packages/services/src/Domain/Device/DatabaseLoadSorter.ts +++ b/packages/services/src/Domain/Device/DatabaseLoadSorter.ts @@ -83,17 +83,25 @@ export function GetSortedPayloadsByPriority { - const payloads: (EncryptedPayloadInterface | DecryptedPayloadInterface)[] = file.items.map((item) => { - if (isEncryptedTransferPayload(item)) { - return new EncryptedPayload(item) - } else if (isDecryptedTransferPayload(item)) { - return new DecryptedPayload(item) - } else { - throw Error('Unhandled case in decryptBackupFile') - } - }) - - const { encrypted, decrypted } = CreatePayloadSplit(payloads) - - const type = getBackupFileType(file, payloads) - - switch (type) { - case BackupFileType.Corrupt: - return new ClientDisplayableError('Invalid backup file.') - case BackupFileType.Encrypted: { - if (!password) { - throw Error('Attempting to decrypt encrypted file with no password') - } - - const keyParamsData = (file.keyParams || file.auth_params) as AnyKeyParamsContent - - return [ - ...decrypted, - ...(await decryptEncrypted(password, CreateAnyKeyParams(keyParamsData), encrypted, protocolService)), - ] - } - case BackupFileType.EncryptedWithNonEncryptedItemsKey: - return [...decrypted, ...(await decryptEncryptedWithNonEncryptedItemsKey(payloads, protocolService))] - case BackupFileType.FullyDecrypted: - return [...decrypted, ...encrypted] - } -} - -function getBackupFileType(file: BackupFile, payloads: PayloadInterface[]): BackupFileType { - if (file.keyParams || file.auth_params) { - return BackupFileType.Encrypted - } else { - const hasEncryptedItem = payloads.find(isEncryptedPayload) - const hasDecryptedItemsKey = payloads.find( - (payload) => payload.content_type === ContentType.ItemsKey && isDecryptedPayload(payload), - ) - - if (hasEncryptedItem && hasDecryptedItemsKey) { - return BackupFileType.EncryptedWithNonEncryptedItemsKey - } else if (!hasEncryptedItem) { - return BackupFileType.FullyDecrypted - } else { - return BackupFileType.Corrupt - } - } -} - -async function decryptEncryptedWithNonEncryptedItemsKey( - allPayloads: (EncryptedPayloadInterface | DecryptedPayloadInterface)[], - protocolService: EncryptionService, -): Promise<(EncryptedPayloadInterface | DecryptedPayloadInterface)[]> { - const decryptedItemsKeys: DecryptedPayloadInterface[] = [] - const encryptedPayloads: EncryptedPayloadInterface[] = [] - - allPayloads.forEach((payload) => { - if (payload.content_type === ContentType.ItemsKey && isDecryptedPayload(payload)) { - decryptedItemsKeys.push(payload as DecryptedPayloadInterface) - } else if (isEncryptedPayload(payload)) { - encryptedPayloads.push(payload) - } - }) - - const itemsKeys = decryptedItemsKeys.map((p) => CreateDecryptedItemFromPayload(p)) - - return decryptWithItemsKeys(encryptedPayloads, itemsKeys, protocolService) -} - -function findKeyToUseForPayload( - payload: EncryptedPayloadInterface, - availableKeys: ItemsKeyInterface[], - protocolService: EncryptionService, - keyParams?: SNRootKeyParams, - fallbackRootKey?: SNRootKey, -): ItemsKeyInterface | SNRootKey | undefined { - let itemsKey: ItemsKeyInterface | SNRootKey | undefined - - if (payload.items_key_id) { - itemsKey = protocolService.itemsKeyForPayload(payload) - if (itemsKey) { - return itemsKey - } - } - - itemsKey = availableKeys.find((itemsKeyPayload) => { - return payload.items_key_id === itemsKeyPayload.uuid - }) - - if (itemsKey) { - return itemsKey - } - - if (!keyParams) { - return undefined - } - - const payloadVersion = payload.version as ProtocolVersion - - /** - * Payloads with versions <= 003 use root key directly for encryption. - * However, if the incoming key params are >= 004, this means we should - * have an items key based off the 003 root key. We can't use the 004 - * root key directly because it's missing dataAuthenticationKey. - */ - if (leftVersionGreaterThanOrEqualToRight(keyParams.version, ProtocolVersion.V004)) { - itemsKey = protocolService.defaultItemsKeyForItemVersion(payloadVersion, availableKeys) - } else if (compareVersions(payloadVersion, ProtocolVersion.V003) <= 0) { - itemsKey = fallbackRootKey - } - - return itemsKey -} - -async function decryptWithItemsKeys( - payloads: EncryptedPayloadInterface[], - itemsKeys: ItemsKeyInterface[], - protocolService: EncryptionService, - keyParams?: SNRootKeyParams, - fallbackRootKey?: SNRootKey, -): Promise<(DecryptedPayloadInterface | EncryptedPayloadInterface)[]> { - const results: (DecryptedPayloadInterface | EncryptedPayloadInterface)[] = [] - - for (const encryptedPayload of payloads) { - if (ContentTypeUsesRootKeyEncryption(encryptedPayload.content_type)) { - continue - } - - try { - const key = findKeyToUseForPayload(encryptedPayload, itemsKeys, protocolService, keyParams, fallbackRootKey) - - if (!key) { - results.push( - encryptedPayload.copy({ - errorDecrypting: true, - }), - ) - continue - } - - if (isItemsKey(key)) { - const decryptedPayload = await protocolService.decryptSplitSingle({ - usesItemsKey: { - items: [encryptedPayload], - key: key, - }, - }) - results.push(decryptedPayload) - } else { - const decryptedPayload = await protocolService.decryptSplitSingle({ - usesRootKey: { - items: [encryptedPayload], - key: key, - }, - }) - results.push(decryptedPayload) - } - } catch (e) { - results.push( - encryptedPayload.copy({ - errorDecrypting: true, - }), - ) - console.error('Error decrypting payload', encryptedPayload, e) - } - } - - return results -} - -async function decryptEncrypted( - password: string, - keyParams: SNRootKeyParams, - payloads: EncryptedPayloadInterface[], - protocolService: EncryptionService, -): Promise<(EncryptedPayloadInterface | DecryptedPayloadInterface)[]> { - const results: (EncryptedPayloadInterface | DecryptedPayloadInterface)[] = [] - const rootKey = await protocolService.computeRootKey(password, keyParams) - - const itemsKeysPayloads = payloads.filter((payload) => { - return payload.content_type === ContentType.ItemsKey - }) - - const itemsKeysDecryptionResults = await protocolService.decryptSplit({ - usesRootKey: { - items: itemsKeysPayloads, - key: rootKey, - }, - }) - - extendArray(results, itemsKeysDecryptionResults) - - const decryptedPayloads = await decryptWithItemsKeys( - payloads, - itemsKeysDecryptionResults.filter(isDecryptedPayload).map((p) => CreateDecryptedItemFromPayload(p)), - protocolService, - keyParams, - rootKey, - ) - - extendArray(results, decryptedPayloads) - - return results -} diff --git a/packages/services/src/Domain/Encryption/DecryptBackupFileUseCase.ts b/packages/services/src/Domain/Encryption/DecryptBackupFileUseCase.ts new file mode 100644 index 000000000..ec3dc5d7e --- /dev/null +++ b/packages/services/src/Domain/Encryption/DecryptBackupFileUseCase.ts @@ -0,0 +1,282 @@ +import { + AnyKeyParamsContent, + compareVersions, + ContentType, + leftVersionGreaterThanOrEqualToRight, + ProtocolVersion, +} from '@standardnotes/common' +import { + BackupFileType, + CreateAnyKeyParams, + isItemsKey, + isKeySystemItemsKey, + SNItemsKey, + SplitPayloadsByEncryptionType, +} from '@standardnotes/encryption' +import { + ContentTypeUsesKeySystemRootKeyEncryption, + ContentTypeUsesRootKeyEncryption, + BackupFile, + CreateDecryptedItemFromPayload, + CreatePayloadSplit, + DecryptedPayload, + DecryptedPayloadInterface, + EncryptedPayload, + EncryptedPayloadInterface, + isDecryptedPayload, + isDecryptedTransferPayload, + isEncryptedPayload, + isEncryptedTransferPayload, + ItemsKeyContent, + ItemsKeyInterface, + PayloadInterface, + KeySystemItemsKeyInterface, + RootKeyInterface, + KeySystemRootKeyInterface, + isKeySystemRootKey, + RootKeyParamsInterface, +} from '@standardnotes/models' +import { ClientDisplayableError } from '@standardnotes/responses' +import { extendArray } from '@standardnotes/utils' +import { EncryptionService } from './EncryptionService' + +export class DecryptBackupFileUseCase { + constructor(private encryption: EncryptionService) {} + + async execute( + file: BackupFile, + password?: string, + ): Promise { + const payloads: (EncryptedPayloadInterface | DecryptedPayloadInterface)[] = file.items.map((item) => { + if (isEncryptedTransferPayload(item)) { + return new EncryptedPayload(item) + } else if (isDecryptedTransferPayload(item)) { + return new DecryptedPayload(item) + } else { + throw Error('Unhandled case in decryptBackupFile') + } + }) + + const { encrypted, decrypted } = CreatePayloadSplit(payloads) + + const type = this.getBackupFileType(file, payloads) + + switch (type) { + case BackupFileType.Corrupt: + return new ClientDisplayableError('Invalid backup file.') + case BackupFileType.Encrypted: { + if (!password) { + throw Error('Attempting to decrypt encrypted file with no password') + } + + const keyParamsData = (file.keyParams || file.auth_params) as AnyKeyParamsContent + + const rootKey = await this.encryption.computeRootKey(password, CreateAnyKeyParams(keyParamsData)) + + const results = await this.decryptEncrypted({ + password, + payloads: encrypted, + rootKey, + keyParams: CreateAnyKeyParams(keyParamsData), + }) + + return [...decrypted, ...results] + } + case BackupFileType.EncryptedWithNonEncryptedItemsKey: + return [...decrypted, ...(await this.decryptEncryptedWithNonEncryptedItemsKey(payloads))] + case BackupFileType.FullyDecrypted: + return [...decrypted, ...encrypted] + } + } + + private getBackupFileType(file: BackupFile, payloads: PayloadInterface[]): BackupFileType { + if (file.keyParams || file.auth_params) { + return BackupFileType.Encrypted + } else { + const hasEncryptedItem = payloads.find(isEncryptedPayload) + const hasDecryptedItemsKey = payloads.find( + (payload) => payload.content_type === ContentType.ItemsKey && isDecryptedPayload(payload), + ) + + if (hasEncryptedItem && hasDecryptedItemsKey) { + return BackupFileType.EncryptedWithNonEncryptedItemsKey + } else if (!hasEncryptedItem) { + return BackupFileType.FullyDecrypted + } else { + return BackupFileType.Corrupt + } + } + } + + private async decryptEncrypted(dto: { + password: string + keyParams: RootKeyParamsInterface + payloads: EncryptedPayloadInterface[] + rootKey: RootKeyInterface + }): Promise<(EncryptedPayloadInterface | DecryptedPayloadInterface)[]> { + const results: (EncryptedPayloadInterface | DecryptedPayloadInterface)[] = [] + + const { rootKeyEncryption, itemsKeyEncryption } = SplitPayloadsByEncryptionType(dto.payloads) + + const rootKeyBasedDecryptionResults = await this.encryption.decryptSplit({ + usesRootKey: { + items: rootKeyEncryption || [], + key: dto.rootKey, + }, + }) + + extendArray(results, rootKeyBasedDecryptionResults) + + const decryptedPayloads = await this.decrypt({ + payloads: itemsKeyEncryption || [], + availableItemsKeys: rootKeyBasedDecryptionResults + .filter(isItemsKey) + .filter(isDecryptedPayload) + .map((p) => CreateDecryptedItemFromPayload(p)), + keyParams: dto.keyParams, + rootKey: dto.rootKey, + }) + + extendArray(results, decryptedPayloads) + + return results + } + + private async decryptEncryptedWithNonEncryptedItemsKey( + payloads: (EncryptedPayloadInterface | DecryptedPayloadInterface)[], + ): Promise<(EncryptedPayloadInterface | DecryptedPayloadInterface)[]> { + const decryptedItemsKeys: DecryptedPayloadInterface[] = [] + const encryptedPayloads: EncryptedPayloadInterface[] = [] + + payloads.forEach((payload) => { + if (payload.content_type === ContentType.ItemsKey && isDecryptedPayload(payload)) { + decryptedItemsKeys.push(payload as DecryptedPayloadInterface) + } else if (isEncryptedPayload(payload)) { + encryptedPayloads.push(payload) + } + }) + + const itemsKeys = decryptedItemsKeys.map((p) => CreateDecryptedItemFromPayload(p)) + + return this.decrypt({ payloads: encryptedPayloads, availableItemsKeys: itemsKeys, rootKey: undefined }) + } + + private findKeyToUseForPayload(dto: { + payload: EncryptedPayloadInterface + availableKeys: ItemsKeyInterface[] + keyParams?: RootKeyParamsInterface + rootKey?: RootKeyInterface + }): ItemsKeyInterface | RootKeyInterface | KeySystemRootKeyInterface | KeySystemItemsKeyInterface | undefined { + if (ContentTypeUsesRootKeyEncryption(dto.payload.content_type)) { + if (!dto.rootKey) { + throw new Error('Attempting to decrypt root key encrypted payload with no root key') + } + return dto.rootKey + } + + if (ContentTypeUsesKeySystemRootKeyEncryption(dto.payload.content_type)) { + throw new Error('Backup file key system root key encryption is not supported') + } + + let itemsKey: ItemsKeyInterface | RootKeyInterface | KeySystemItemsKeyInterface | undefined + + if (dto.payload.items_key_id) { + itemsKey = this.encryption.itemsKeyForEncryptedPayload(dto.payload) + if (itemsKey) { + return itemsKey + } + } + + itemsKey = dto.availableKeys.find((itemsKeyPayload) => { + return dto.payload.items_key_id === itemsKeyPayload.uuid + }) + + if (itemsKey) { + return itemsKey + } + + if (!dto.keyParams) { + return undefined + } + + const payloadVersion = dto.payload.version as ProtocolVersion + + /** + * Payloads with versions <= 003 use root key directly for encryption. + * However, if the incoming key params are >= 004, this means we should + * have an items key based off the 003 root key. We can't use the 004 + * root key directly because it's missing dataAuthenticationKey. + */ + if (leftVersionGreaterThanOrEqualToRight(dto.keyParams.version, ProtocolVersion.V004)) { + itemsKey = this.encryption.defaultItemsKeyForItemVersion(payloadVersion, dto.availableKeys) + } else if (compareVersions(payloadVersion, ProtocolVersion.V003) <= 0) { + itemsKey = dto.rootKey + } + + return itemsKey + } + + private async decrypt(dto: { + payloads: EncryptedPayloadInterface[] + availableItemsKeys: ItemsKeyInterface[] + rootKey: RootKeyInterface | undefined + keyParams?: RootKeyParamsInterface + }): Promise<(DecryptedPayloadInterface | EncryptedPayloadInterface)[]> { + const results: (DecryptedPayloadInterface | EncryptedPayloadInterface)[] = [] + + for (const encryptedPayload of dto.payloads) { + try { + const key = this.findKeyToUseForPayload({ + payload: encryptedPayload, + availableKeys: dto.availableItemsKeys, + keyParams: dto.keyParams, + rootKey: dto.rootKey, + }) + + if (!key) { + results.push( + encryptedPayload.copy({ + errorDecrypting: true, + }), + ) + continue + } + + if (isItemsKey(key) || isKeySystemItemsKey(key)) { + const decryptedPayload = await this.encryption.decryptSplitSingle({ + usesItemsKey: { + items: [encryptedPayload], + key: key, + }, + }) + results.push(decryptedPayload) + } else if (isKeySystemRootKey(key)) { + const decryptedPayload = await this.encryption.decryptSplitSingle({ + usesKeySystemRootKey: { + items: [encryptedPayload], + key: key, + }, + }) + results.push(decryptedPayload) + } else { + const decryptedPayload = await this.encryption.decryptSplitSingle({ + usesRootKey: { + items: [encryptedPayload], + key: key, + }, + }) + results.push(decryptedPayload) + } + } catch (e) { + results.push( + encryptedPayload.copy({ + errorDecrypting: true, + }), + ) + console.error('Error decrypting payload', encryptedPayload, e) + } + } + + return results + } +} diff --git a/packages/services/src/Domain/Encryption/EncryptionService.ts b/packages/services/src/Domain/Encryption/EncryptionService.ts index bb1da6216..0892f544f 100644 --- a/packages/services/src/Domain/Encryption/EncryptionService.ts +++ b/packages/services/src/Domain/Encryption/EncryptionService.ts @@ -1,9 +1,8 @@ +import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' import { CreateAnyKeyParams, CreateEncryptionSplitWithKeyLookup, - DecryptedParameters, - EncryptedParameters, - encryptedParametersFromPayload, + encryptedInputParametersFromPayload, EncryptionProviderInterface, ErrorDecryptingParameters, findDefaultItemsKey, @@ -23,6 +22,11 @@ import { SplitPayloadsByEncryptionType, V001Algorithm, V002Algorithm, + PublicKeySet, + EncryptedOutputParameters, + KeySystemKeyManagerInterface, + AsymmetricSignatureVerificationDetachedResult, + AsymmetricallyEncryptedString, } from '@standardnotes/encryption' import { BackupFile, @@ -37,9 +41,15 @@ import { ItemContent, ItemsKeyInterface, RootKeyInterface, + KeySystemItemsKeyInterface, + KeySystemIdentifier, + AsymmetricMessagePayload, + KeySystemRootKeyInterface, + KeySystemRootKeyParamsInterface, + TrustedContactInterface, } from '@standardnotes/models' import { ClientDisplayableError } from '@standardnotes/responses' -import { PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common' import { extendArray, isNotUndefined, @@ -68,10 +78,10 @@ import { DeviceInterface } from '../Device/DeviceInterface' import { StorageServiceInterface } from '../Storage/StorageServiceInterface' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { SyncEvent } from '../Event/SyncEvent' -import { DiagnosticInfo } from '../Diagnostics/ServiceDiagnostics' import { RootKeyEncryptionService } from './RootKeyEncryption' -import { DecryptBackupFile } from './BackupFileDecryptor' +import { DecryptBackupFileUseCase } from './DecryptBackupFileUseCase' import { EncryptionServiceEvent } from './EncryptionServiceEvent' +import { DecryptedParameters } from '@standardnotes/encryption/src/Domain/Types/DecryptedParameters' /** * The encryption service is responsible for the encryption and decryption of payloads, and @@ -108,9 +118,11 @@ export class EncryptionService extends AbstractService i constructor( private itemManager: ItemManagerInterface, + private mutator: MutatorClientInterface, private payloadManager: PayloadManagerInterface, public deviceInterface: DeviceInterface, private storageService: StorageServiceInterface, + public readonly keys: KeySystemKeyManagerInterface, private identifier: ApplicationIdentifier, public crypto: PureCryptoInterface, protected override internalEventBus: InternalEventBusInterface, @@ -125,17 +137,22 @@ export class EncryptionService extends AbstractService i payloadManager, storageService, this.operatorManager, + keys, internalEventBus, ) this.rootKeyEncryption = new RootKeyEncryptionService( this.itemManager, + this.mutator, this.operatorManager, this.deviceInterface, this.storageService, + this.payloadManager, + keys, this.identifier, this.internalEventBus, ) + this.rootKeyObserverDisposer = this.rootKeyEncryption.addEventObserver((event) => { this.itemsEncryption.userVersion = this.getUserVersion() if (event === RootKeyServiceEvent.RootKeyStatusChanged) { @@ -166,6 +183,32 @@ export class EncryptionService extends AbstractService i super.deinit() } + /** @throws */ + getKeyPair(): PkcKeyPair { + const rootKey = this.getRootKey() + + if (!rootKey?.encryptionKeyPair) { + throw new Error('Account keypair not found') + } + + return rootKey.encryptionKeyPair + } + + /** @throws */ + getSigningKeyPair(): PkcKeyPair { + const rootKey = this.getRootKey() + + if (!rootKey?.signingKeyPair) { + throw new Error('Account keypair not found') + } + + return rootKey.signingKeyPair + } + + hasSigningKeyPair(): boolean { + return !!this.getRootKey()?.signingKeyPair + } + public async initialize() { await this.rootKeyEncryption.initialize() } @@ -213,8 +256,12 @@ export class EncryptionService extends AbstractService i return this.itemsEncryption.repersistAllItems() } - public async reencryptItemsKeys(): Promise { - await this.rootKeyEncryption.reencryptItemsKeys() + public async reencryptApplicableItemsAfterUserRootKeyChange(): Promise { + await this.rootKeyEncryption.reencryptApplicableItemsAfterUserRootKeyChange() + } + + public reencryptKeySystemItemsKeysForVault(keySystemIdentifier: KeySystemIdentifier): Promise { + return this.rootKeyEncryption.reencryptKeySystemItemsKeysForVault(keySystemIdentifier) } public async createNewItemsKeyWithRollback(): Promise<() => Promise> { @@ -222,11 +269,14 @@ export class EncryptionService extends AbstractService i } public async decryptErroredPayloads(): Promise { - await this.itemsEncryption.decryptErroredPayloads() + await this.rootKeyEncryption.decryptErroredRootPayloads() + await this.itemsEncryption.decryptErroredItemPayloads() } - public itemsKeyForPayload(payload: EncryptedPayloadInterface): ItemsKeyInterface | undefined { - return this.itemsEncryption.itemsKeyForPayload(payload) + public itemsKeyForEncryptedPayload( + payload: EncryptedPayloadInterface, + ): ItemsKeyInterface | KeySystemItemsKeyInterface | undefined { + return this.itemsEncryption.itemsKeyForEncryptedPayload(payload) } public defaultItemsKeyForItemVersion( @@ -241,34 +291,66 @@ export class EncryptionService extends AbstractService i } public async encryptSplit(split: KeyedEncryptionSplit): Promise { - const allEncryptedParams: EncryptedParameters[] = [] + const allEncryptedParams: EncryptedOutputParameters[] = [] - if (split.usesRootKey) { + const { + usesRootKey, + usesItemsKey, + usesKeySystemRootKey, + usesRootKeyWithKeyLookup, + usesItemsKeyWithKeyLookup, + usesKeySystemRootKeyWithKeyLookup, + } = split + + const signingKeyPair = this.hasSigningKeyPair() ? this.getSigningKeyPair() : undefined + + if (usesRootKey) { const rootKeyEncrypted = await this.rootKeyEncryption.encryptPayloads( - split.usesRootKey.items, - split.usesRootKey.key, + usesRootKey.items, + usesRootKey.key, + signingKeyPair, ) extendArray(allEncryptedParams, rootKeyEncrypted) } - if (split.usesItemsKey) { + if (usesRootKeyWithKeyLookup) { + const rootKeyEncrypted = await this.rootKeyEncryption.encryptPayloadsWithKeyLookup( + usesRootKeyWithKeyLookup.items, + signingKeyPair, + ) + extendArray(allEncryptedParams, rootKeyEncrypted) + } + + if (usesKeySystemRootKey) { + const keySystemRootKeyEncrypted = await this.rootKeyEncryption.encryptPayloads( + usesKeySystemRootKey.items, + usesKeySystemRootKey.key, + signingKeyPair, + ) + extendArray(allEncryptedParams, keySystemRootKeyEncrypted) + } + + if (usesKeySystemRootKeyWithKeyLookup) { + const keySystemRootKeyEncrypted = await this.rootKeyEncryption.encryptPayloadsWithKeyLookup( + usesKeySystemRootKeyWithKeyLookup.items, + signingKeyPair, + ) + extendArray(allEncryptedParams, keySystemRootKeyEncrypted) + } + + if (usesItemsKey) { const itemsKeyEncrypted = await this.itemsEncryption.encryptPayloads( - split.usesItemsKey.items, - split.usesItemsKey.key, + usesItemsKey.items, + usesItemsKey.key, + signingKeyPair, ) extendArray(allEncryptedParams, itemsKeyEncrypted) } - if (split.usesRootKeyWithKeyLookup) { - const rootKeyEncrypted = await this.rootKeyEncryption.encryptPayloadsWithKeyLookup( - split.usesRootKeyWithKeyLookup.items, - ) - extendArray(allEncryptedParams, rootKeyEncrypted) - } - - if (split.usesItemsKeyWithKeyLookup) { + if (usesItemsKeyWithKeyLookup) { const itemsKeyEncrypted = await this.itemsEncryption.encryptPayloadsWithKeyLookup( - split.usesItemsKeyWithKeyLookup.items, + usesItemsKeyWithKeyLookup.items, + signingKeyPair, ) extendArray(allEncryptedParams, itemsKeyEncrypted) } @@ -300,32 +382,48 @@ export class EncryptionService extends AbstractService i >(split: KeyedDecryptionSplit): Promise<(P | EncryptedPayloadInterface)[]> { const resultParams: (DecryptedParameters | ErrorDecryptingParameters)[] = [] - if (split.usesRootKey) { - const rootKeyDecrypted = await this.rootKeyEncryption.decryptPayloads( - split.usesRootKey.items, - split.usesRootKey.key, - ) + const { + usesRootKey, + usesItemsKey, + usesKeySystemRootKey, + usesRootKeyWithKeyLookup, + usesItemsKeyWithKeyLookup, + usesKeySystemRootKeyWithKeyLookup, + } = split + + if (usesRootKey) { + const rootKeyDecrypted = await this.rootKeyEncryption.decryptPayloads(usesRootKey.items, usesRootKey.key) extendArray(resultParams, rootKeyDecrypted) } - if (split.usesRootKeyWithKeyLookup) { + if (usesRootKeyWithKeyLookup) { const rootKeyDecrypted = await this.rootKeyEncryption.decryptPayloadsWithKeyLookup( - split.usesRootKeyWithKeyLookup.items, + usesRootKeyWithKeyLookup.items, ) extendArray(resultParams, rootKeyDecrypted) } - - if (split.usesItemsKey) { - const itemsKeyDecrypted = await this.itemsEncryption.decryptPayloads( - split.usesItemsKey.items, - split.usesItemsKey.key, + if (usesKeySystemRootKey) { + const keySystemRootKeyDecrypted = await this.rootKeyEncryption.decryptPayloads( + usesKeySystemRootKey.items, + usesKeySystemRootKey.key, ) + extendArray(resultParams, keySystemRootKeyDecrypted) + } + if (usesKeySystemRootKeyWithKeyLookup) { + const keySystemRootKeyDecrypted = await this.rootKeyEncryption.decryptPayloadsWithKeyLookup( + usesKeySystemRootKeyWithKeyLookup.items, + ) + extendArray(resultParams, keySystemRootKeyDecrypted) + } + + if (usesItemsKey) { + const itemsKeyDecrypted = await this.itemsEncryption.decryptPayloads(usesItemsKey.items, usesItemsKey.key) extendArray(resultParams, itemsKeyDecrypted) } - if (split.usesItemsKeyWithKeyLookup) { + if (usesItemsKeyWithKeyLookup) { const itemsKeyDecrypted = await this.itemsEncryption.decryptPayloadsWithKeyLookup( - split.usesItemsKeyWithKeyLookup.items, + usesItemsKeyWithKeyLookup.items, ) extendArray(resultParams, itemsKeyDecrypted) } @@ -349,6 +447,36 @@ export class EncryptionService extends AbstractService i return packagedResults } + async decryptPayloadWithKeyLookup< + C extends ItemContent = ItemContent, + P extends DecryptedPayloadInterface = DecryptedPayloadInterface, + >( + payload: EncryptedPayloadInterface, + ): Promise<{ + parameters: DecryptedParameters | ErrorDecryptingParameters + payload: P | EncryptedPayloadInterface + }> { + const decryptedParameters = await this.itemsEncryption.decryptPayloadWithKeyLookup(payload) + + if (isErrorDecryptingParameters(decryptedParameters)) { + return { + parameters: decryptedParameters, + payload: new EncryptedPayload({ + ...payload.ejected(), + ...decryptedParameters, + }), + } + } else { + return { + parameters: decryptedParameters, + payload: new DecryptedPayload({ + ...payload.ejected(), + ...decryptedParameters, + }) as P, + } + } + } + /** * Returns true if the user's account protocol version is not equal to the latest version. */ @@ -420,27 +548,130 @@ export class EncryptionService extends AbstractService i * Computes a root key given a password and key params. * Delegates computation to respective protocol operator. */ - public async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise { + public async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise { return this.rootKeyEncryption.computeRootKey(password, keyParams) } /** * Creates a root key using the latest protocol version */ - public async createRootKey( + public async createRootKey( identifier: string, password: string, origination: KeyParamsOrigination, version?: ProtocolVersion, - ) { + ): Promise { return this.rootKeyEncryption.createRootKey(identifier, password, origination, version) } + createRandomizedKeySystemRootKey(dto: { + systemIdentifier: KeySystemIdentifier + systemName: string + systemDescription?: string + }): KeySystemRootKeyInterface { + return this.operatorManager.defaultOperator().createRandomizedKeySystemRootKey(dto) + } + + createUserInputtedKeySystemRootKey(dto: { + systemIdentifier: KeySystemIdentifier + systemName: string + systemDescription?: string + userInputtedPassword: string + }): KeySystemRootKeyInterface { + return this.operatorManager.defaultOperator().createUserInputtedKeySystemRootKey(dto) + } + + deriveUserInputtedKeySystemRootKey(dto: { + keyParams: KeySystemRootKeyParamsInterface + userInputtedPassword: string + }): KeySystemRootKeyInterface { + return this.operatorManager.defaultOperator().deriveUserInputtedKeySystemRootKey(dto) + } + + createKeySystemItemsKey( + uuid: string, + keySystemIdentifier: KeySystemIdentifier, + sharedVaultUuid: string | undefined, + rootKeyToken: string, + ): KeySystemItemsKeyInterface { + return this.operatorManager + .defaultOperator() + .createKeySystemItemsKey(uuid, keySystemIdentifier, sharedVaultUuid, rootKeyToken) + } + + asymmetricallyEncryptMessage(dto: { + message: AsymmetricMessagePayload + senderKeyPair: PkcKeyPair + senderSigningKeyPair: PkcKeyPair + recipientPublicKey: string + }): AsymmetricallyEncryptedString { + const operator = this.operatorManager.defaultOperator() + const encrypted = operator.asymmetricEncrypt({ + stringToEncrypt: JSON.stringify(dto.message), + senderKeyPair: dto.senderKeyPair, + senderSigningKeyPair: dto.senderSigningKeyPair, + recipientPublicKey: dto.recipientPublicKey, + }) + return encrypted + } + + asymmetricallyDecryptMessage(dto: { + encryptedString: AsymmetricallyEncryptedString + trustedSender: TrustedContactInterface | undefined + privateKey: string + }): M | undefined { + const defaultOperator = this.operatorManager.defaultOperator() + const version = defaultOperator.versionForAsymmetricallyEncryptedString(dto.encryptedString) + const keyOperator = this.operatorManager.operatorForVersion(version) + const decryptedResult = keyOperator.asymmetricDecrypt({ + stringToDecrypt: dto.encryptedString, + recipientSecretKey: dto.privateKey, + }) + + if (!decryptedResult) { + return undefined + } + + if (!decryptedResult.signatureVerified) { + return undefined + } + + if (dto.trustedSender) { + if (!dto.trustedSender.isPublicKeyTrusted(decryptedResult.senderPublicKey)) { + return undefined + } + + if (!dto.trustedSender.isSigningKeyTrusted(decryptedResult.signaturePublicKey)) { + return undefined + } + } + + return JSON.parse(decryptedResult.plaintext) + } + + asymmetricSignatureVerifyDetached( + encryptedString: AsymmetricallyEncryptedString, + ): AsymmetricSignatureVerificationDetachedResult { + const defaultOperator = this.operatorManager.defaultOperator() + const version = defaultOperator.versionForAsymmetricallyEncryptedString(encryptedString) + const keyOperator = this.operatorManager.operatorForVersion(version) + return keyOperator.asymmetricSignatureVerifyDetached(encryptedString) + } + + getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: AsymmetricallyEncryptedString): PublicKeySet { + const defaultOperator = this.operatorManager.defaultOperator() + const version = defaultOperator.versionForAsymmetricallyEncryptedString(string) + + const keyOperator = this.operatorManager.operatorForVersion(version) + return keyOperator.getSenderPublicKeySetFromAsymmetricallyEncryptedString(string) + } + public async decryptBackupFile( file: BackupFile, password?: string, ): Promise)[]> { - const result = await DecryptBackupFile(file, this, password) + const usecase = new DecryptBackupFileUseCase(this) + const result = await usecase.execute(file, password) return result } @@ -468,7 +699,7 @@ export class EncryptionService extends AbstractService i items: ejected, } - const keyParams = await this.getRootKeyParams() + const keyParams = this.getRootKeyParams() data.keyParams = keyParams?.getPortableValue() return data } @@ -504,7 +735,7 @@ export class EncryptionService extends AbstractService i return (await this.rootKeyEncryption.hasRootKeyWrapper()) && this.rootKeyEncryption.getRootKey() == undefined } - public async getRootKeyParams() { + public getRootKeyParams() { return this.rootKeyEncryption.getRootKeyParams() } @@ -517,7 +748,7 @@ export class EncryptionService extends AbstractService i * Wrapping key params are read from disk. */ public async computeWrappingKey(passcode: string) { - const keyParams = await this.rootKeyEncryption.getSureRootKeyWrapperKeyParams() + const keyParams = this.rootKeyEncryption.getSureRootKeyWrapperKeyParams() const key = await this.computeRootKey(passcode, keyParams) return key } @@ -545,17 +776,21 @@ export class EncryptionService extends AbstractService i await this.rootKeyEncryption.removeRootKeyWrapper() } - public async setRootKey(key: SNRootKey, wrappingKey?: SNRootKey) { + public async setRootKey(key: RootKeyInterface, wrappingKey?: SNRootKey) { await this.rootKeyEncryption.setRootKey(key, wrappingKey) } /** * Returns the in-memory root key value. */ - public getRootKey() { + public getRootKey(): RootKeyInterface | undefined { return this.rootKeyEncryption.getRootKey() } + public getSureRootKey(): RootKeyInterface { + return this.rootKeyEncryption.getRootKey() as RootKeyInterface + } + /** * Deletes root key and wrapper from keychain. Used when signing out of application. */ @@ -571,26 +806,31 @@ export class EncryptionService extends AbstractService i return this.rootKeyEncryption.validatePasscode(passcode) } - public getEmbeddedPayloadAuthenticatedData( + public getEmbeddedPayloadAuthenticatedData( payload: EncryptedPayloadInterface, - ): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined { + ): D | undefined { const version = payload.version if (!version) { return undefined } + const operator = this.operatorManager.operatorForVersion(version) - const authenticatedData = operator.getPayloadAuthenticatedData(encryptedParametersFromPayload(payload)) - return authenticatedData + + const authenticatedData = operator.getPayloadAuthenticatedDataForExternalUse( + encryptedInputParametersFromPayload(payload), + ) + + return authenticatedData as D } /** Returns the key params attached to this key's encrypted payload */ - public getKeyEmbeddedKeyParams(key: EncryptedPayloadInterface): SNRootKeyParams | undefined { + public getKeyEmbeddedKeyParamsFromItemsKey(key: EncryptedPayloadInterface): SNRootKeyParams | undefined { const authenticatedData = this.getEmbeddedPayloadAuthenticatedData(key) if (!authenticatedData) { return undefined } if (isVersionLessThanOrEqualTo(key.version, ProtocolVersion.V003)) { - const rawKeyParams = authenticatedData as LegacyAttachedData + const rawKeyParams = authenticatedData as unknown as LegacyAttachedData return this.createKeyParams(rawKeyParams) } else { const rawKeyParams = (authenticatedData as RootKeyEncryptedAuthenticatedData).kp @@ -683,7 +923,7 @@ export class EncryptionService extends AbstractService i const hasSyncedItemsKey = !isNullOrUndefined(defaultSyncedKey) if (hasSyncedItemsKey) { /** Delete all never synced keys */ - await this.itemManager.setItemsToBeDeleted(neverSyncedKeys) + await this.mutator.setItemsToBeDeleted(neverSyncedKeys) } else { /** * No previous synced items key. @@ -692,14 +932,14 @@ export class EncryptionService extends AbstractService i * we end up with 0 items keys, create a new one. This covers the case when you open * the app offline and it creates an 004 key, and then you sign into an 003 account. */ - const rootKeyParams = await this.getRootKeyParams() + const rootKeyParams = this.getRootKeyParams() if (rootKeyParams) { /** If neverSynced.version != rootKey.version, delete. */ const toDelete = neverSyncedKeys.filter((itemsKey) => { return itemsKey.keyVersion !== rootKeyParams.version }) if (toDelete.length > 0) { - await this.itemManager.setItemsToBeDeleted(toDelete) + await this.mutator.setItemsToBeDeleted(toDelete) } if (this.itemsEncryption.getItemsKeys().length === 0) { @@ -741,26 +981,7 @@ export class EncryptionService extends AbstractService i const unsyncedKeys = this.itemsEncryption.getItemsKeys().filter((key) => key.neverSynced && !key.dirty) if (unsyncedKeys.length > 0) { - void this.itemManager.setItemsDirty(unsyncedKeys) - } - } - - override async getDiagnostics(): Promise { - return { - encryption: { - getLatestVersion: this.getLatestVersion(), - hasAccount: this.hasAccount(), - hasRootKeyEncryptionSource: this.hasRootKeyEncryptionSource(), - getUserVersion: this.getUserVersion(), - upgradeAvailable: await this.upgradeAvailable(), - accountUpgradeAvailable: this.accountUpgradeAvailable(), - passcodeUpgradeAvailable: await this.passcodeUpgradeAvailable(), - hasPasscode: this.hasPasscode(), - isPasscodeLocked: await this.isPasscodeLocked(), - needsNewRootKeyBasedItemsKey: this.needsNewRootKeyBasedItemsKey(), - ...(await this.itemsEncryption.getDiagnostics()), - ...(await this.rootKeyEncryption.getDiagnostics()), - }, + void this.mutator.setItemsDirty(unsyncedKeys) } } } diff --git a/packages/services/src/Domain/Encryption/Functions.ts b/packages/services/src/Domain/Encryption/Functions.ts index 41e464e91..aa3ef03e8 100644 --- a/packages/services/src/Domain/Encryption/Functions.ts +++ b/packages/services/src/Domain/Encryption/Functions.ts @@ -49,7 +49,7 @@ export async function DecryptItemsKeyByPromptingUser( | 'aborted' > { if (!keyParams) { - keyParams = encryptor.getKeyEmbeddedKeyParams(itemsKey) + keyParams = encryptor.getKeyEmbeddedKeyParamsFromItemsKey(itemsKey) } if (!keyParams) { diff --git a/packages/services/src/Domain/Encryption/ItemsEncryption.ts b/packages/services/src/Domain/Encryption/ItemsEncryption.ts index 9e1f6a124..d7475446f 100644 --- a/packages/services/src/Domain/Encryption/ItemsEncryption.ts +++ b/packages/services/src/Domain/Encryption/ItemsEncryption.ts @@ -1,7 +1,6 @@ import { ContentType, ProtocolVersion } from '@standardnotes/common' import { DecryptedParameters, - EncryptedParameters, ErrorDecryptingParameters, findDefaultItemsKey, isErrorDecryptingParameters, @@ -9,26 +8,30 @@ import { StandardException, encryptPayload, decryptPayload, + EncryptedOutputParameters, + KeySystemKeyManagerInterface, } from '@standardnotes/encryption' import { + ContentTypeUsesKeySystemRootKeyEncryption, DecryptedPayload, DecryptedPayloadInterface, EncryptedPayload, EncryptedPayloadInterface, + KeySystemRootKeyInterface, isEncryptedPayload, ItemContent, ItemsKeyInterface, PayloadEmitSource, + KeySystemItemsKeyInterface, SureFindPayload, + ContentTypeUsesRootKeyEncryption, } from '@standardnotes/models' -import { Uuids } from '@standardnotes/utils' - -import { DiagnosticInfo } from '../Diagnostics/ServiceDiagnostics' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { ItemManagerInterface } from '../Item/ItemManagerInterface' import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface' import { AbstractService } from '../Service/AbstractService' import { StorageServiceInterface } from '../Storage/StorageServiceInterface' +import { PkcKeyPair } from '@standardnotes/sncrypto-common' export class ItemsEncryptionService extends AbstractService { private removeItemsObserver!: () => void @@ -39,13 +42,14 @@ export class ItemsEncryptionService extends AbstractService { private payloadManager: PayloadManagerInterface, private storageService: StorageServiceInterface, private operatorManager: OperatorManager, + private keys: KeySystemKeyManagerInterface, protected override internalEventBus: InternalEventBusInterface, ) { super(internalEventBus) this.removeItemsObserver = this.itemManager.addObserver([ContentType.ItemsKey], ({ changed, inserted }) => { if (changed.concat(inserted).length > 0) { - void this.decryptErroredPayloads() + void this.decryptErroredItemPayloads() } }) } @@ -54,6 +58,8 @@ export class ItemsEncryptionService extends AbstractService { ;(this.itemManager as unknown) = undefined ;(this.payloadManager as unknown) = undefined ;(this.storageService as unknown) = undefined + ;(this.operatorManager as unknown) = undefined + ;(this.keys as unknown) = undefined this.removeItemsObserver() ;(this.removeItemsObserver as unknown) = undefined super.deinit() @@ -70,12 +76,17 @@ export class ItemsEncryptionService extends AbstractService { return this.storageService.savePayloads(payloads) } - public getItemsKeys() { + public getItemsKeys(): ItemsKeyInterface[] { return this.itemManager.getDisplayableItemsKeys() } - public itemsKeyForPayload(payload: EncryptedPayloadInterface): ItemsKeyInterface | undefined { - return this.getItemsKeys().find( + public itemsKeyForEncryptedPayload( + payload: EncryptedPayloadInterface, + ): ItemsKeyInterface | KeySystemItemsKeyInterface | undefined { + const itemsKeys = this.getItemsKeys() + const keySystemItemsKeys = this.itemManager.getItems(ContentType.KeySystemItemsKey) + + return [...itemsKeys, ...keySystemItemsKeys].find( (key) => key.uuid === payload.items_key_id || key.duplicateOf === payload.items_key_id, ) } @@ -84,8 +95,20 @@ export class ItemsEncryptionService extends AbstractService { return findDefaultItemsKey(this.getItemsKeys()) } - private keyToUseForItemEncryption(): ItemsKeyInterface | StandardException { + private keyToUseForItemEncryption( + payload: DecryptedPayloadInterface, + ): ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | StandardException { + if (payload.key_system_identifier) { + const keySystemItemsKey = this.keys.getPrimaryKeySystemItemsKey(payload.key_system_identifier) + if (!keySystemItemsKey) { + return new StandardException('Cannot find key system items key to use for encryption') + } + + return keySystemItemsKey + } + const defaultKey = this.getDefaultItemsKey() + let result: ItemsKeyInterface | undefined = undefined if (this.userVersion && this.userVersion !== defaultKey?.keyVersion) { @@ -107,9 +130,11 @@ export class ItemsEncryptionService extends AbstractService { return result } - private keyToUseForDecryptionOfPayload(payload: EncryptedPayloadInterface): ItemsKeyInterface | undefined { + private keyToUseForDecryptionOfPayload( + payload: EncryptedPayloadInterface, + ): ItemsKeyInterface | KeySystemItemsKeyInterface | undefined { if (payload.items_key_id) { - const itemsKey = this.itemsKeyForPayload(payload) + const itemsKey = this.itemsKeyForEncryptedPayload(payload) return itemsKey } @@ -117,20 +142,24 @@ export class ItemsEncryptionService extends AbstractService { return defaultKey } - public async encryptPayloadWithKeyLookup(payload: DecryptedPayloadInterface): Promise { - const key = this.keyToUseForItemEncryption() + public async encryptPayloadWithKeyLookup( + payload: DecryptedPayloadInterface, + signingKeyPair?: PkcKeyPair, + ): Promise { + const key = this.keyToUseForItemEncryption(payload) if (key instanceof StandardException) { throw Error(key.message) } - return this.encryptPayload(payload, key) + return this.encryptPayload(payload, key, signingKeyPair) } public async encryptPayload( payload: DecryptedPayloadInterface, - key: ItemsKeyInterface, - ): Promise { + key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface, + signingKeyPair?: PkcKeyPair, + ): Promise { if (isEncryptedPayload(payload)) { throw Error('Attempting to encrypt already encrypted payload.') } @@ -141,18 +170,22 @@ export class ItemsEncryptionService extends AbstractService { throw Error('Attempting to encrypt payload with no UuidGenerator.') } - return encryptPayload(payload, key, this.operatorManager) + return encryptPayload(payload, key, this.operatorManager, signingKeyPair) } public async encryptPayloads( payloads: DecryptedPayloadInterface[], - key: ItemsKeyInterface, - ): Promise { - return Promise.all(payloads.map((payload) => this.encryptPayload(payload, key))) + key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface, + signingKeyPair?: PkcKeyPair, + ): Promise { + return Promise.all(payloads.map((payload) => this.encryptPayload(payload, key, signingKeyPair))) } - public async encryptPayloadsWithKeyLookup(payloads: DecryptedPayloadInterface[]): Promise { - return Promise.all(payloads.map((payload) => this.encryptPayloadWithKeyLookup(payload))) + public async encryptPayloadsWithKeyLookup( + payloads: DecryptedPayloadInterface[], + signingKeyPair?: PkcKeyPair, + ): Promise { + return Promise.all(payloads.map((payload) => this.encryptPayloadWithKeyLookup(payload, signingKeyPair))) } public async decryptPayloadWithKeyLookup( @@ -173,7 +206,7 @@ export class ItemsEncryptionService extends AbstractService { public async decryptPayload( payload: EncryptedPayloadInterface, - key: ItemsKeyInterface, + key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface, ): Promise | ErrorDecryptingParameters> { if (!payload.content) { return { @@ -193,21 +226,24 @@ export class ItemsEncryptionService extends AbstractService { public async decryptPayloads( payloads: EncryptedPayloadInterface[], - key: ItemsKeyInterface, + key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface, ): Promise<(DecryptedParameters | ErrorDecryptingParameters)[]> { return Promise.all(payloads.map((payload) => this.decryptPayload(payload, key))) } - public async decryptErroredPayloads(): Promise { - const payloads = this.payloadManager.invalidPayloads.filter((i) => i.content_type !== ContentType.ItemsKey) - if (payloads.length === 0) { + public async decryptErroredItemPayloads(): Promise { + const erroredItemPayloads = this.payloadManager.invalidPayloads.filter( + (i) => + !ContentTypeUsesRootKeyEncryption(i.content_type) && !ContentTypeUsesKeySystemRootKeyEncryption(i.content_type), + ) + if (erroredItemPayloads.length === 0) { return } - const resultParams = await this.decryptPayloadsWithKeyLookup(payloads) + const resultParams = await this.decryptPayloadsWithKeyLookup(erroredItemPayloads) const decryptedPayloads = resultParams.map((params) => { - const original = SureFindPayload(payloads, params.uuid) + const original = SureFindPayload(erroredItemPayloads, params.uuid) if (isErrorDecryptingParameters(params)) { return new EncryptedPayload({ ...original.ejected(), @@ -247,15 +283,4 @@ export class ItemsEncryptionService extends AbstractService { return key.keyVersion === version }) } - - override async getDiagnostics(): Promise { - const keyForItems = this.keyToUseForItemEncryption() - return { - itemsEncryption: { - itemsKeysIds: Uuids(this.getItemsKeys()), - defaultItemsKeyId: this.getDefaultItemsKey()?.uuid, - keyToUseForItemEncryptionId: keyForItems instanceof StandardException ? undefined : keyForItems.uuid, - }, - } - } } diff --git a/packages/services/src/Domain/Encryption/RootKeyEncryption.ts b/packages/services/src/Domain/Encryption/RootKeyEncryption.ts index 67e97f57e..c5994840f 100644 --- a/packages/services/src/Domain/Encryption/RootKeyEncryption.ts +++ b/packages/services/src/Domain/Encryption/RootKeyEncryption.ts @@ -1,3 +1,4 @@ +import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' import { ApplicationIdentifier, ProtocolVersionLatest, @@ -17,15 +18,19 @@ import { CreateAnyKeyParams, SNRootKey, isErrorDecryptingParameters, - EncryptedParameters, - DecryptedParameters, ErrorDecryptingParameters, findDefaultItemsKey, ItemsKeyMutator, encryptPayload, decryptPayload, + EncryptedOutputParameters, + DecryptedParameters, + KeySystemKeyManagerInterface, } from '@standardnotes/encryption' import { + ContentTypeUsesKeySystemRootKeyEncryption, + ContentTypesUsingRootKeyEncryption, + ContentTypeUsesRootKeyEncryption, CreateDecryptedItemFromPayload, DecryptedPayload, DecryptedPayloadInterface, @@ -34,25 +39,29 @@ import { EncryptedPayloadInterface, EncryptedTransferPayload, FillItemContentSpecialized, + KeySystemRootKeyInterface, ItemContent, ItemsKeyContent, ItemsKeyContentSpecialized, ItemsKeyInterface, NamespacedRootKeyInKeychain, + PayloadEmitSource, PayloadTimestampDefaults, RootKeyContent, RootKeyInterface, + SureFindPayload, + KeySystemIdentifier, } from '@standardnotes/models' import { UuidGenerator } from '@standardnotes/utils' - import { DeviceInterface } from '../Device/DeviceInterface' -import { DiagnosticInfo } from '../Diagnostics/ServiceDiagnostics' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { ItemManagerInterface } from '../Item/ItemManagerInterface' import { AbstractService } from '../Service/AbstractService' import { StorageKey } from '../Storage/StorageKeys' import { StorageServiceInterface } from '../Storage/StorageServiceInterface' import { StorageValueModes } from '../Storage/StorageTypes' +import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface' +import { PkcKeyPair } from '@standardnotes/sncrypto-common' export class RootKeyEncryptionService extends AbstractService { private rootKey?: RootKeyInterface @@ -60,10 +69,13 @@ export class RootKeyEncryptionService extends AbstractService { - const rawKeyParams = await this.storageService.getValue( - StorageKey.RootKeyWrapperKeyParams, - StorageValueModes.Nonwrapped, - ) + public getRootKeyWrapperKeyParams(): SNRootKeyParams | undefined { + const rawKeyParams = this.storageService.getValue(StorageKey.RootKeyWrapperKeyParams, StorageValueModes.Nonwrapped) if (!rawKeyParams) { return undefined @@ -206,11 +221,11 @@ export class RootKeyEncryptionService extends AbstractService + public getSureRootKeyWrapperKeyParams() { + return this.getRootKeyWrapperKeyParams() as SNRootKeyParams } - public async getRootKeyParams(): Promise { + public getRootKeyParams(): SNRootKeyParams | undefined { if (this.keyMode === KeyMode.WrapperOnly) { return this.getRootKeyWrapperKeyParams() } else if (this.keyMode === KeyMode.RootKeyOnly || this.keyMode === KeyMode.RootKeyPlusWrapper) { @@ -222,22 +237,22 @@ export class RootKeyEncryptionService extends AbstractService { - return this.getRootKeyParams() as Promise + public getSureRootKeyParams(): SNRootKeyParams { + return this.getRootKeyParams() as SNRootKeyParams } - public async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise { + public async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise { const version = keyParams.version const operator = this.operatorManager.operatorForVersion(version) return operator.computeRootKey(password, keyParams) } - public async createRootKey( + public async createRootKey( identifier: string, password: string, origination: KeyParamsOrigination, version?: ProtocolVersion, - ) { + ): Promise { const operator = version ? this.operatorManager.operatorForVersion(version) : this.operatorManager.defaultOperator() return operator.createRootKey(identifier, password, origination) } @@ -291,8 +306,8 @@ export class RootKeyEncryptionService extends AbstractService { - const rawKeyParams = await this.storageService.getValue(StorageKey.RootKeyParams, StorageValueModes.Nonwrapped) + private recomputeAccountKeyParams(): SNRootKeyParams | undefined { + const rawKeyParams = this.storageService.getValue(StorageKey.RootKeyParams, StorageValueModes.Nonwrapped) if (!rawKeyParams) { return @@ -308,10 +323,12 @@ export class RootKeyEncryptionService extends AbstractService { - const key = this.getRootKey() + private async encryptPayloadWithKeyLookup( + payload: DecryptedPayloadInterface, + signingKeyPair?: PkcKeyPair, + ): Promise { + let key: RootKeyInterface | KeySystemRootKeyInterface | undefined + if (ContentTypeUsesKeySystemRootKeyEncryption(payload.content_type)) { + if (!payload.key_system_identifier) { + throw Error(`Key system-encrypted payload ${payload.content_type}is missing a key_system_identifier`) + } + key = this.keys.getPrimaryKeySystemRootKey(payload.key_system_identifier) + } else { + key = this.getRootKey() + } if (key == undefined) { throw Error('Attempting root key encryption with no root key') } - return this.encryptPayload(payload, key) + return this.encryptPayload(payload, key, signingKeyPair) } - public async encryptPayloadsWithKeyLookup(payloads: DecryptedPayloadInterface[]): Promise { - return Promise.all(payloads.map((payload) => this.encrypPayloadWithKeyLookup(payload))) + public async encryptPayloadsWithKeyLookup( + payloads: DecryptedPayloadInterface[], + signingKeyPair?: PkcKeyPair, + ): Promise { + return Promise.all(payloads.map((payload) => this.encryptPayloadWithKeyLookup(payload, signingKeyPair))) } - public async encryptPayload(payload: DecryptedPayloadInterface, key: RootKeyInterface): Promise { - return encryptPayload(payload, key, this.operatorManager) + public async encryptPayload( + payload: DecryptedPayloadInterface, + key: RootKeyInterface | KeySystemRootKeyInterface, + signingKeyPair?: PkcKeyPair, + ): Promise { + return encryptPayload(payload, key, this.operatorManager, signingKeyPair) } - public async encryptPayloads(payloads: DecryptedPayloadInterface[], key: RootKeyInterface) { - return Promise.all(payloads.map((payload) => this.encryptPayload(payload, key))) + public async encryptPayloads( + payloads: DecryptedPayloadInterface[], + key: RootKeyInterface | KeySystemRootKeyInterface, + signingKeyPair?: PkcKeyPair, + ) { + return Promise.all(payloads.map((payload) => this.encryptPayload(payload, key, signingKeyPair))) } public async decryptPayloadWithKeyLookup( payload: EncryptedPayloadInterface, ): Promise | ErrorDecryptingParameters> { - const key = this.getRootKey() + let key: RootKeyInterface | KeySystemRootKeyInterface | undefined + if (ContentTypeUsesKeySystemRootKeyEncryption(payload.content_type)) { + if (!payload.key_system_identifier) { + throw Error('Key system root key encrypted payload is missing key_system_identifier') + } + key = this.keys.getPrimaryKeySystemRootKey(payload.key_system_identifier) + } else { + key = this.getRootKey() + } if (key == undefined) { return { @@ -530,7 +577,7 @@ export class RootKeyEncryptionService extends AbstractService( payload: EncryptedPayloadInterface, - key: RootKeyInterface, + key: RootKeyInterface | KeySystemRootKeyInterface, ): Promise | ErrorDecryptingParameters> { return decryptPayload(payload, key, this.operatorManager) } @@ -543,25 +590,63 @@ export class RootKeyEncryptionService extends AbstractService( payloads: EncryptedPayloadInterface[], - key: RootKeyInterface, + key: RootKeyInterface | KeySystemRootKeyInterface, ): Promise<(DecryptedParameters | ErrorDecryptingParameters)[]> { return Promise.all(payloads.map((payload) => this.decryptPayload(payload, key))) } - /** - * When the root key changes (non-null only), we must re-encrypt all items - * keys with this new root key (by simply re-syncing). - */ - public async reencryptItemsKeys(): Promise { - const itemsKeys = this.getItemsKeys() + public async decryptErroredRootPayloads(): Promise { + const erroredRootPayloads = this.payloadManager.invalidPayloads.filter( + (i) => + ContentTypeUsesRootKeyEncryption(i.content_type) || ContentTypeUsesKeySystemRootKeyEncryption(i.content_type), + ) + if (erroredRootPayloads.length === 0) { + return + } - if (itemsKeys.length > 0) { + const resultParams = await this.decryptPayloadsWithKeyLookup(erroredRootPayloads) + + const decryptedPayloads = resultParams.map((params) => { + const original = SureFindPayload(erroredRootPayloads, params.uuid) + if (isErrorDecryptingParameters(params)) { + return new EncryptedPayload({ + ...original.ejected(), + ...params, + }) + } else { + return new DecryptedPayload({ + ...original.ejected(), + ...params, + }) + } + }) + + await this.payloadManager.emitPayloads(decryptedPayloads, PayloadEmitSource.LocalChanged) + } + + /** + * When the root key changes, we must re-encrypt all relevant items with this new root key (by simply re-syncing). + */ + public async reencryptApplicableItemsAfterUserRootKeyChange(): Promise { + const items = this.items.getItems(ContentTypesUsingRootKeyEncryption()) + if (items.length > 0) { /** * Do not call sync after marking dirty. * Re-encrypting items keys is called by consumers who have specific flows who * will sync on their own timing */ - await this.itemManager.setItemsDirty(itemsKeys) + await this.mutator.setItemsDirty(items) + } + } + + /** + * When the key system root key changes, we must re-encrypt all vault items keys + * with this new key system root key (by simply re-syncing). + */ + public async reencryptKeySystemItemsKeysForVault(keySystemIdentifier: KeySystemIdentifier): Promise { + const keySystemItemsKeys = this.keys.getKeySystemItemsKeys(keySystemIdentifier) + if (keySystemItemsKeys.length > 0) { + await this.mutator.setItemsDirty(keySystemItemsKeys) } } @@ -599,14 +684,13 @@ export class RootKeyEncryptionService extends AbstractService { + await this.mutator.changeItemsKey(key, (mutator) => { mutator.isDefault = false }) } - const itemsKey = (await this.itemManager.insertItem(itemTemplate)) as ItemsKeyInterface - - await this.itemManager.changeItemsKey(itemsKey, (mutator) => { + const itemsKey = await this.mutator.insertItem(itemTemplate) + await this.mutator.changeItemsKey(itemsKey, (mutator) => { mutator.isDefault = true }) @@ -618,10 +702,10 @@ export class RootKeyEncryptionService extends AbstractService { - await this.itemManager.setItemToBeDeleted(newDefaultItemsKey) + await this.mutator.setItemToBeDeleted(newDefaultItemsKey) if (currentDefaultItemsKey) { - await this.itemManager.changeItem(currentDefaultItemsKey, (mutator) => { + await this.mutator.changeItem(currentDefaultItemsKey, (mutator) => { mutator.isDefault = true }) } @@ -629,19 +713,4 @@ export class RootKeyEncryptionService extends AbstractService { - return { - rootKeyEncryption: { - hasRootKey: this.rootKey != undefined, - keyMode: KeyMode[this.keyMode], - hasRootKeyWrapper: await this.hasRootKeyWrapper(), - hasAccount: this.hasAccount(), - hasRootKeyEncryptionSource: this.hasRootKeyEncryptionSource(), - hasPasscode: this.hasPasscode(), - getEncryptionSourceVersion: this.hasRootKeyEncryptionSource() && (await this.getEncryptionSourceVersion()), - getUserVersion: this.getUserVersion(), - }, - } - } } diff --git a/packages/services/src/Domain/Event/ApplicationEvent.ts b/packages/services/src/Domain/Event/ApplicationEvent.ts index df4d8d7b3..b30f3dd43 100644 --- a/packages/services/src/Domain/Event/ApplicationEvent.ts +++ b/packages/services/src/Domain/Event/ApplicationEvent.ts @@ -1,68 +1,79 @@ +import { ApplicationStage } from './../Application/ApplicationStage' export enum ApplicationEvent { - SignedIn = 2, - SignedOut = 3, + SignedIn = 'signed-in', + SignedOut = 'signed-out', /** When a full, potentially multi-page sync completes */ - CompletedFullSync = 5, + CompletedFullSync = 'completed-full-sync', - FailedSync = 6, - HighLatencySync = 7, - EnteredOutOfSync = 8, - ExitedOutOfSync = 9, + FailedSync = 'failed-sync', + HighLatencySync = 'high-latency-sync', + EnteredOutOfSync = 'entered-out-of-sync', + ExitedOutOfSync = 'exited-out-of-sync', + + ApplicationStageChanged = 'application-stage-changed', /** - * The application has finished it `prepareForLaunch` state and is now ready for unlock + * The application has finished its prepareForLaunch state and is now ready for unlock * Called when the application has initialized and is ready for launch, but before * the application has been unlocked, if applicable. Use this to do pre-launch * configuration, but do not attempt to access user data like notes or tags. */ - Started = 10, + Started = 'started', /** * The applicaiton is fully unlocked and ready for i/o * Called when the application has been fully decrypted and unlocked. Use this to * to begin streaming data like notes and tags. */ - Launched = 11, - LocalDataLoaded = 12, + Launched = 'launched', + + LocalDataLoaded = 'local-data-loaded', /** * When the root key or root key wrapper changes. Includes events like account state * changes (registering, signing in, changing pw, logging out) and passcode state * changes (adding, removing, changing). */ - KeyStatusChanged = 13, + KeyStatusChanged = 'key-status-changed', - MajorDataChange = 14, - CompletedRestart = 15, - LocalDataIncrementalLoad = 16, - SyncStatusChanged = 17, - WillSync = 18, - InvalidSyncSession = 19, - LocalDatabaseReadError = 20, - LocalDatabaseWriteError = 21, + MajorDataChange = 'major-data-change', + CompletedRestart = 'completed-restart', + LocalDataIncrementalLoad = 'local-data-incremental-load', + SyncStatusChanged = 'sync-status-changed', + WillSync = 'will-sync', + InvalidSyncSession = 'invalid-sync-session', + LocalDatabaseReadError = 'local-database-read-error', + LocalDatabaseWriteError = 'local-database-write-error', - /** When a single roundtrip completes with sync, in a potentially multi-page sync request. - * If just a single roundtrip, this event will be triggered, along with CompletedFullSync */ - CompletedIncrementalSync = 22, + /** + * When a single roundtrip completes with sync, in a potentially multi-page sync request. + * If just a single roundtrip, this event will be triggered, along with CompletedFullSync + */ + CompletedIncrementalSync = 'completed-incremental-sync', /** * The application has loaded all pending migrations (but not run any, except for the base one), - * and consumers may now call `hasPendingMigrations` + * and consumers may now call hasPendingMigrations */ - MigrationsLoaded = 23, + MigrationsLoaded = 'migrations-loaded', - /** When StorageService is ready to start servicing read/write requests */ - StorageReady = 24, + /** When StorageService is ready (but NOT yet decrypted) to start servicing read/write requests */ + StorageReady = 'storage-ready', + + PreferencesChanged = 'preferences-changed', + UnprotectedSessionBegan = 'unprotected-session-began', + UserRolesChanged = 'user-roles-changed', + FeaturesUpdated = 'features-updated', + UnprotectedSessionExpired = 'unprotected-session-expired', - PreferencesChanged = 25, - UnprotectedSessionBegan = 26, - UserRolesChanged = 27, - FeaturesUpdated = 28, - UnprotectedSessionExpired = 29, /** Called when the app first launches and after first sync request made after sign in */ - CompletedInitialSync = 30, - BiometricsSoftLockEngaged = 31, - BiometricsSoftLockDisengaged = 32, - DidPurchaseSubscription = 33, + CompletedInitialSync = 'completed-initial-sync', + BiometricsSoftLockEngaged = 'biometrics-soft-lock-engaged', + BiometricsSoftLockDisengaged = 'biometrics-soft-lock-disengaged', + DidPurchaseSubscription = 'did-purchase-subscription', +} + +export type ApplicationStageChangedEventPayload = { + stage: ApplicationStage } diff --git a/packages/services/src/Domain/Event/SyncEvent.ts b/packages/services/src/Domain/Event/SyncEvent.ts index 6e6ff3dc4..3fddfa3a7 100644 --- a/packages/services/src/Domain/Event/SyncEvent.ts +++ b/packages/services/src/Domain/Event/SyncEvent.ts @@ -1,3 +1,10 @@ +import { + AsymmetricMessageServerHash, + SharedVaultInviteServerHash, + SharedVaultServerHash, + UserEventServerHash, +} from '@standardnotes/responses' + /* istanbul ignore file */ export enum SyncEvent { /** @@ -7,8 +14,8 @@ export enum SyncEvent { */ SyncCompletedWithAllItemsUploaded = 'SyncCompletedWithAllItemsUploaded', SyncCompletedWithAllItemsUploadedAndDownloaded = 'SyncCompletedWithAllItemsUploadedAndDownloaded', - SingleRoundTripSyncCompleted = 'SingleRoundTripSyncCompleted', - SyncWillBegin = 'sync:will-begin', + PaginatedSyncRequestCompleted = 'PaginatedSyncRequestCompleted', + SyncDidBeginProcessing = 'sync:did-begin-processing', DownloadFirstSyncCompleted = 'sync:download-first-completed', SyncTakingTooLong = 'sync:taking-too-long', SyncError = 'sync:error', @@ -22,4 +29,13 @@ export enum SyncEvent { DatabaseWriteError = 'database-write-error', DatabaseReadError = 'database-read-error', SyncRequestsIntegrityCheck = 'sync:requests-integrity-check', + ReceivedRemoteSharedVaults = 'received-shared-vaults', + ReceivedSharedVaultInvites = 'received-shared-vault-invites', + ReceivedUserEvents = 'received-user-events', + ReceivedAsymmetricMessages = 'received-asymmetric-messages', } + +export type SyncEventReceivedRemoteSharedVaultsData = SharedVaultServerHash[] +export type SyncEventReceivedSharedVaultInvitesData = SharedVaultInviteServerHash[] +export type SyncEventReceivedAsymmetricMessagesData = AsymmetricMessageServerHash[] +export type SyncEventReceivedUserEventsData = UserEventServerHash[] diff --git a/packages/services/src/Domain/Files/FileService.spec.ts b/packages/services/src/Domain/Files/FileService.spec.ts index f17e797df..49b674484 100644 --- a/packages/services/src/Domain/Files/FileService.spec.ts +++ b/packages/services/src/Domain/Files/FileService.spec.ts @@ -3,16 +3,18 @@ import { FileItem } from '@standardnotes/models' import { EncryptionProviderInterface } from '@standardnotes/encryption' import { ItemManagerInterface } from '../Item/ItemManagerInterface' import { ChallengeServiceInterface } from '../Challenge' -import { InternalEventBusInterface } from '..' +import { InternalEventBusInterface, MutatorClientInterface } from '..' import { AlertService } from '../Alert/AlertService' import { ApiServiceInterface } from '../Api/ApiServiceInterface' import { SyncServiceInterface } from '../Sync/SyncServiceInterface' import { FileService } from './FileService' import { BackupServiceInterface } from '@standardnotes/files' +import { HttpServiceInterface } from '@standardnotes/api' describe('fileService', () => { let apiService: ApiServiceInterface let itemManager: ItemManagerInterface + let mutator: MutatorClientInterface let syncService: SyncServiceInterface let alertService: AlertService let crypto: PureCryptoInterface @@ -21,26 +23,28 @@ describe('fileService', () => { let encryptor: EncryptionProviderInterface let internalEventBus: InternalEventBusInterface let backupService: BackupServiceInterface + let http: HttpServiceInterface beforeEach(() => { apiService = {} as jest.Mocked apiService.addEventObserver = jest.fn() - apiService.createFileValetToken = jest.fn() + apiService.createUserFileValetToken = jest.fn() apiService.deleteFile = jest.fn().mockReturnValue({}) const numChunks = 1 apiService.downloadFile = jest .fn() .mockImplementation( - ( - _file: string, - _chunkIndex: number, - _apiToken: string, - _rangeStart: number, - onBytesReceived: (bytes: Uint8Array) => void, - ) => { + (params: { + _file: string + _chunkIndex: number + _apiToken: string + _ownershipType: string + _rangeStart: number + onBytesReceived: (bytes: Uint8Array) => void + }) => { return new Promise((resolve) => { for (let i = 0; i < numChunks; i++) { - onBytesReceived(Uint8Array.from([0xaa])) + params.onBytesReceived(Uint8Array.from([0xaa])) } resolve() @@ -49,11 +53,13 @@ describe('fileService', () => { ) itemManager = {} as jest.Mocked - itemManager.createItem = jest.fn() itemManager.createTemplateItem = jest.fn().mockReturnValue({}) - itemManager.setItemToBeDeleted = jest.fn() itemManager.addObserver = jest.fn() - itemManager.changeItem = jest.fn() + + mutator = {} as jest.Mocked + mutator.createItem = jest.fn() + mutator.setItemToBeDeleted = jest.fn() + mutator.changeItem = jest.fn() challengor = {} as jest.Mocked @@ -75,12 +81,15 @@ describe('fileService', () => { backupService.readEncryptedFileFromBackup = jest.fn() backupService.getFileBackupInfo = jest.fn() + http = {} as jest.Mocked + fileService = new FileService( apiService, - itemManager, + mutator, syncService, encryptor, challengor, + http, alertService, crypto, internalEventBus, @@ -152,7 +161,7 @@ describe('fileService', () => { } as jest.Mocked const alertMock = (alertService.confirm = jest.fn().mockReturnValue(true)) - const deleteItemMock = (itemManager.setItemToBeDeleted = jest.fn()) + const deleteItemMock = (mutator.setItemToBeDeleted = jest.fn()) apiService.deleteFile = jest.fn().mockReturnValue({ data: { error: true } }) diff --git a/packages/services/src/Domain/Files/FileService.ts b/packages/services/src/Domain/Files/FileService.ts index 6483cc787..87a311b0c 100644 --- a/packages/services/src/Domain/Files/FileService.ts +++ b/packages/services/src/Domain/Files/FileService.ts @@ -1,4 +1,10 @@ -import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses' +import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' +import { + ClientDisplayableError, + ValetTokenOperation, + isClientDisplayableError, + isErrorResponse, +} from '@standardnotes/responses' import { ContentType } from '@standardnotes/common' import { FileItem, @@ -9,6 +15,8 @@ import { FileContent, EncryptedPayload, isEncryptedPayload, + VaultListingInterface, + SharedVaultListingInterface, } from '@standardnotes/models' import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { spaceSeparatedStrings, UuidGenerator } from '@standardnotes/utils' @@ -36,29 +44,37 @@ import { import { AlertService, ButtonType } from '../Alert/AlertService' import { ChallengeServiceInterface } from '../Challenge' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' -import { ItemManagerInterface } from '../Item/ItemManagerInterface' import { AbstractService } from '../Service/AbstractService' import { SyncServiceInterface } from '../Sync/SyncServiceInterface' import { DecryptItemsKeyWithUserFallback } from '../Encryption/Functions' import { log, LoggingDomain } from '../Logging' +import { + SharedVaultMoveType, + SharedVaultServer, + SharedVaultServerInterface, + HttpServiceInterface, +} from '@standardnotes/api' const OneHundredMb = 100 * 1_000_000 export class FileService extends AbstractService implements FilesClientInterface { private encryptedCache: FileMemoryCache = new FileMemoryCache(OneHundredMb) + private sharedVault: SharedVaultServerInterface constructor( private api: FilesApiInterface, - private itemManager: ItemManagerInterface, + private mutator: MutatorClientInterface, private syncService: SyncServiceInterface, private encryptor: EncryptionProviderInterface, private challengor: ChallengeServiceInterface, + http: HttpServiceInterface, private alertService: AlertService, private crypto: PureCryptoInterface, protected override internalEventBus: InternalEventBusInterface, private backupsService?: BackupServiceInterface, ) { super(internalEventBus) + this.sharedVault = new SharedVaultServer(http) } override deinit(): void { @@ -67,7 +83,6 @@ export class FileService extends AbstractService implements FilesClientInterface this.encryptedCache.clear() ;(this.encryptedCache as unknown) = undefined ;(this.api as unknown) = undefined - ;(this.itemManager as unknown) = undefined ;(this.encryptor as unknown) = undefined ;(this.syncService as unknown) = undefined ;(this.alertService as unknown) = undefined @@ -79,14 +94,109 @@ export class FileService extends AbstractService implements FilesClientInterface return 5_000_000 } + private async createUserValetToken( + remoteIdentifier: string, + operation: ValetTokenOperation, + unencryptedFileSizeForUpload?: number | undefined, + ): Promise { + return this.api.createUserFileValetToken(remoteIdentifier, operation, unencryptedFileSizeForUpload) + } + + private async createSharedVaultValetToken(params: { + sharedVaultUuid: string + remoteIdentifier: string + operation: ValetTokenOperation + fileUuidRequiredForExistingFiles?: string + unencryptedFileSizeForUpload?: number | undefined + moveOperationType?: SharedVaultMoveType + sharedVaultToSharedVaultMoveTargetUuid?: string + }): Promise { + if (params.operation !== 'write' && !params.fileUuidRequiredForExistingFiles) { + throw new Error('File UUID is required for for non-write operations') + } + + const valetTokenResponse = await this.sharedVault.createSharedVaultFileValetToken({ + sharedVaultUuid: params.sharedVaultUuid, + fileUuid: params.fileUuidRequiredForExistingFiles, + remoteIdentifier: params.remoteIdentifier, + operation: params.operation, + unencryptedFileSize: params.unencryptedFileSizeForUpload, + moveOperationType: params.moveOperationType, + sharedVaultToSharedVaultMoveTargetUuid: params.sharedVaultToSharedVaultMoveTargetUuid, + }) + + if (isErrorResponse(valetTokenResponse)) { + return new ClientDisplayableError('Could not create valet token') + } + + return valetTokenResponse.data.valetToken + } + + public async moveFileToSharedVault( + file: FileItem, + sharedVault: SharedVaultListingInterface, + ): Promise { + const valetTokenResult = await this.createSharedVaultValetToken({ + sharedVaultUuid: file.shared_vault_uuid ? file.shared_vault_uuid : sharedVault.sharing.sharedVaultUuid, + remoteIdentifier: file.remoteIdentifier, + operation: 'move', + fileUuidRequiredForExistingFiles: file.uuid, + moveOperationType: file.shared_vault_uuid ? 'shared-vault-to-shared-vault' : 'user-to-shared-vault', + sharedVaultToSharedVaultMoveTargetUuid: file.shared_vault_uuid ? sharedVault.sharing.sharedVaultUuid : undefined, + }) + + if (isClientDisplayableError(valetTokenResult)) { + return valetTokenResult + } + + const moveResult = await this.api.moveFile(valetTokenResult) + + if (!moveResult) { + return new ClientDisplayableError('Could not move file') + } + } + + public async moveFileOutOfSharedVault(file: FileItem): Promise { + if (!file.shared_vault_uuid) { + return new ClientDisplayableError('File is not in a shared vault') + } + + const valetTokenResult = await this.createSharedVaultValetToken({ + sharedVaultUuid: file.shared_vault_uuid, + remoteIdentifier: file.remoteIdentifier, + operation: 'move', + fileUuidRequiredForExistingFiles: file.uuid, + moveOperationType: 'shared-vault-to-user', + }) + + if (isClientDisplayableError(valetTokenResult)) { + return valetTokenResult + } + + const moveResult = await this.api.moveFile(valetTokenResult) + + if (!moveResult) { + return new ClientDisplayableError('Could not move file') + } + } + public async beginNewFileUpload( sizeInBytes: number, + vault?: VaultListingInterface, ): Promise { const remoteIdentifier = UuidGenerator.GenerateUuid() - const tokenResult = await this.api.createFileValetToken(remoteIdentifier, 'write', sizeInBytes) + const valetTokenResult = + vault && vault.isSharedVaultListing() + ? await this.createSharedVaultValetToken({ + sharedVaultUuid: vault.sharing.sharedVaultUuid, + remoteIdentifier, + operation: 'write', + unencryptedFileSizeForUpload: sizeInBytes, + }) + : await this.createUserValetToken(remoteIdentifier, 'write', sizeInBytes) - if (tokenResult instanceof ClientDisplayableError) { - return tokenResult + if (valetTokenResult instanceof ClientDisplayableError) { + return valetTokenResult } const key = this.crypto.generateRandomKey(FileProtocolV1Constants.KeySize) @@ -97,9 +207,18 @@ export class FileService extends AbstractService implements FilesClientInterface decryptedSize: sizeInBytes, } - const uploadOperation = new EncryptAndUploadFileOperation(fileParams, tokenResult, this.crypto, this.api) + const uploadOperation = new EncryptAndUploadFileOperation( + fileParams, + valetTokenResult, + this.crypto, + this.api, + vault, + ) - const uploadSessionStarted = await this.api.startUploadSession(tokenResult) + const uploadSessionStarted = await this.api.startUploadSession( + valetTokenResult, + vault && vault.isSharedVaultListing() ? 'shared-vault' : 'user', + ) if (isErrorResponse(uploadSessionStarted) || !uploadSessionStarted.data.uploadId) { return new ClientDisplayableError('Could not start upload session') @@ -127,7 +246,10 @@ export class FileService extends AbstractService implements FilesClientInterface operation: EncryptAndUploadFileOperation, fileMetadata: FileMetadata, ): Promise { - const uploadSessionClosed = await this.api.closeUploadSession(operation.getApiToken()) + const uploadSessionClosed = await this.api.closeUploadSession( + operation.getValetToken(), + operation.vault && operation.vault.isSharedVaultListing() ? 'shared-vault' : 'user', + ) if (!uploadSessionClosed) { return new ClientDisplayableError('Could not close upload session') @@ -145,10 +267,11 @@ export class FileService extends AbstractService implements FilesClientInterface remoteIdentifier: result.remoteIdentifier, } - const file = await this.itemManager.createItem( + const file = await this.mutator.createItem( ContentType.File, FillItemContentSpecialized(fileContent), true, + operation.vault, ) await this.syncService.sync() @@ -215,7 +338,20 @@ export class FileService extends AbstractService implements FilesClientInterface let cacheEntryAggregate = new Uint8Array() - const operation = new DownloadAndDecryptFileOperation(file, this.crypto, this.api) + const tokenResult = file.shared_vault_uuid + ? await this.createSharedVaultValetToken({ + sharedVaultUuid: file.shared_vault_uuid, + remoteIdentifier: file.remoteIdentifier, + operation: 'read', + fileUuidRequiredForExistingFiles: file.uuid, + }) + : await this.createUserValetToken(file.remoteIdentifier, 'read') + + if (tokenResult instanceof ClientDisplayableError) { + return tokenResult + } + + const operation = new DownloadAndDecryptFileOperation(file, this.crypto, this.api, tokenResult) const result = await operation.run(async ({ decrypted, encrypted, progress }): Promise => { if (addToCache) { @@ -235,13 +371,20 @@ export class FileService extends AbstractService implements FilesClientInterface public async deleteFile(file: FileItem): Promise { this.encryptedCache.remove(file.uuid) - const tokenResult = await this.api.createFileValetToken(file.remoteIdentifier, 'delete') + const tokenResult = file.shared_vault_uuid + ? await this.createSharedVaultValetToken({ + sharedVaultUuid: file.shared_vault_uuid, + remoteIdentifier: file.remoteIdentifier, + operation: 'delete', + fileUuidRequiredForExistingFiles: file.uuid, + }) + : await this.createUserValetToken(file.remoteIdentifier, 'delete') if (tokenResult instanceof ClientDisplayableError) { return tokenResult } - const result = await this.api.deleteFile(tokenResult) + const result = await this.api.deleteFile(tokenResult, file.shared_vault_uuid ? 'shared-vault' : 'user') if (result.data?.error) { const deleteAnyway = await this.alertService.confirm( @@ -261,7 +404,7 @@ export class FileService extends AbstractService implements FilesClientInterface } } - await this.itemManager.setItemToBeDeleted(file) + await this.mutator.setItemToBeDeleted(file) await this.syncService.sync() return undefined diff --git a/packages/services/src/Domain/InternalFeatures/InternalFeature.ts b/packages/services/src/Domain/InternalFeatures/InternalFeature.ts new file mode 100644 index 000000000..1dfa14b22 --- /dev/null +++ b/packages/services/src/Domain/InternalFeatures/InternalFeature.ts @@ -0,0 +1,3 @@ +export enum InternalFeature { + Vaults = 'vaults', +} diff --git a/packages/services/src/Domain/InternalFeatures/InternalFeatureService.ts b/packages/services/src/Domain/InternalFeatures/InternalFeatureService.ts new file mode 100644 index 000000000..e30dd2ae5 --- /dev/null +++ b/packages/services/src/Domain/InternalFeatures/InternalFeatureService.ts @@ -0,0 +1,24 @@ +import { InternalFeature } from './InternalFeature' +import { InternalFeatureServiceInterface } from './InternalFeatureServiceInterface' + +let sharedInstance: InternalFeatureServiceInterface | undefined + +export class InternalFeatureService implements InternalFeatureServiceInterface { + static get(): InternalFeatureServiceInterface { + if (!sharedInstance) { + sharedInstance = new InternalFeatureService() + } + return sharedInstance + } + + private readonly enabledFeatures: Set = new Set() + + isFeatureEnabled(feature: InternalFeature): boolean { + return this.enabledFeatures.has(feature) + } + + enableFeature(feature: InternalFeature): void { + console.warn(`Enabling internal feature: ${feature}`) + this.enabledFeatures.add(feature) + } +} diff --git a/packages/services/src/Domain/InternalFeatures/InternalFeatureServiceInterface.ts b/packages/services/src/Domain/InternalFeatures/InternalFeatureServiceInterface.ts new file mode 100644 index 000000000..8c363334f --- /dev/null +++ b/packages/services/src/Domain/InternalFeatures/InternalFeatureServiceInterface.ts @@ -0,0 +1,6 @@ +import { InternalFeature } from './InternalFeature' + +export interface InternalFeatureServiceInterface { + isFeatureEnabled(feature: InternalFeature): boolean + enableFeature(feature: InternalFeature): void +} diff --git a/packages/services/src/Domain/Item/ItemCounterInterface.ts b/packages/services/src/Domain/Item/ItemCounterInterface.ts deleted file mode 100644 index 4c5647d96..000000000 --- a/packages/services/src/Domain/Item/ItemCounterInterface.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { SNNote, SNTag, ItemCounts } from '@standardnotes/models' - -export interface ItemCounterInterface { - countNotesAndTags(items: Array): ItemCounts -} diff --git a/packages/services/src/Domain/Item/ItemManagerInterface.ts b/packages/services/src/Domain/Item/ItemManagerInterface.ts index 95fde44fa..45d271151 100644 --- a/packages/services/src/Domain/Item/ItemManagerInterface.ts +++ b/packages/services/src/Domain/Item/ItemManagerInterface.ts @@ -1,11 +1,7 @@ import { ContentType } from '@standardnotes/common' import { - MutationType, ItemsKeyInterface, - ItemsKeyMutatorInterface, DecryptedItemInterface, - DecryptedItemMutator, - DecryptedPayloadInterface, PayloadEmitSource, EncryptedItemInterface, DeletedItemInterface, @@ -13,6 +9,20 @@ import { PredicateInterface, DecryptedPayload, SNTag, + ItemInterface, + AnyItemInterface, + KeySystemIdentifier, + ItemCollection, + SNNote, + SmartView, + TagItemCountChangeObserver, + SNComponent, + SNTheme, + DecryptedPayloadInterface, + DecryptedTransferPayload, + FileItem, + VaultDisplayOptions, + NotesAndFilesDisplayControllerOptions, } from '@standardnotes/models' import { AbstractService } from '../Service/AbstractService' @@ -41,26 +51,20 @@ export type ItemManagerChangeObserverCallback void export interface ItemManagerInterface extends AbstractService { + getCollection(): ItemCollection + addObserver( contentType: ContentType | ContentType[], callback: ItemManagerChangeObserverCallback, ): () => void - setItemToBeDeleted(itemToLookupUuidFor: DecryptedItemInterface, source?: PayloadEmitSource): Promise - setItemsToBeDeleted(itemsToLookupUuidsFor: DecryptedItemInterface[]): Promise - setItemsDirty( - itemsToLookupUuidsFor: DecryptedItemInterface[], - isUserModified?: boolean, - ): Promise + get items(): DecryptedItemInterface[] - insertItem(item: DecryptedItemInterface): Promise - emitItemFromPayload(payload: DecryptedPayloadInterface, source: PayloadEmitSource): Promise + getItems(contentType: ContentType | ContentType[]): T[] + get invalidItems(): EncryptedItemInterface[] + allTrackedItems(): ItemInterface[] getDisplayableItemsKeys(): ItemsKeyInterface[] - createItem( - contentType: ContentType, - content: C, - needsSync?: boolean, - ): Promise + createTemplateItem< C extends ItemContent = ItemContent, I extends DecryptedItemInterface = DecryptedItemInterface, @@ -69,23 +73,7 @@ export interface ItemManagerInterface extends AbstractService { content?: C, override?: Partial>, ): I - changeItem< - M extends DecryptedItemMutator = DecryptedItemMutator, - I extends DecryptedItemInterface = DecryptedItemInterface, - >( - itemToLookupUuidFor: I, - mutate?: (mutator: M) => void, - mutationType?: MutationType, - emitSource?: PayloadEmitSource, - payloadSourceKey?: string, - ): Promise - changeItemsKey( - itemToLookupUuidFor: ItemsKeyInterface, - mutate: (mutator: ItemsKeyMutatorInterface) => void, - mutationType?: MutationType, - emitSource?: PayloadEmitSource, - payloadSourceKey?: string, - ): Promise + itemsMatchingPredicate( contentType: ContentType, predicate: PredicateInterface, @@ -96,12 +84,47 @@ export interface ItemManagerInterface extends AbstractService { ): T[] subItemsMatchingPredicates(items: T[], predicates: PredicateInterface[]): T[] removeAllItemsFromMemory(): Promise + removeItemsLocally(items: AnyItemInterface[]): void getDirtyItems(): (DecryptedItemInterface | DeletedItemInterface)[] getTagLongTitle(tag: SNTag): string getSortedTagsForItem(item: DecryptedItemInterface): SNTag[] + itemsReferencingItem( + itemToLookupUuidFor: { uuid: string }, + contentType?: ContentType, + ): I[] referencesForItem( itemToLookupUuidFor: DecryptedItemInterface, contentType?: ContentType, ): I[] findItem(uuid: string): T | undefined + findItems(uuids: string[]): T[] + findSureItem(uuid: string): T + get trashedItems(): SNNote[] + itemsBelongingToKeySystem(systemIdentifier: KeySystemIdentifier): DecryptedItemInterface[] + hasTagsNeedingFoldersMigration(): boolean + get invalidNonVaultedItems(): EncryptedItemInterface[] + isTemplateItem(item: DecryptedItemInterface): boolean + getSmartViews(): SmartView[] + addNoteCountChangeObserver(observer: TagItemCountChangeObserver): () => void + allCountableNotesCount(): number + allCountableFilesCount(): number + countableNotesForTag(tag: SNTag | SmartView): number + getNoteCount(): number + getDisplayableTags(): SNTag[] + getTagChildren(itemToLookupUuidFor: SNTag): SNTag[] + getTagParent(itemToLookupUuidFor: SNTag): SNTag | undefined + isValidTagParent(parentTagToLookUpUuidFor: SNTag, childToLookUpUuidFor: SNTag): boolean + isSmartViewTitle(title: string): boolean + getDisplayableComponents(): (SNComponent | SNTheme)[] + createItemFromPayload(payload: DecryptedPayloadInterface): DecryptedItemInterface + createPayloadFromObject(object: DecryptedTransferPayload): DecryptedPayloadInterface + getDisplayableFiles(): FileItem[] + setVaultDisplayOptions(options: VaultDisplayOptions): void + numberOfNotesWithConflicts(): number + getDisplayableNotes(): SNNote[] + getDisplayableNotesAndFiles(): (SNNote | FileItem)[] + setPrimaryItemDisplayOptions(options: NotesAndFilesDisplayControllerOptions): void + getTagPrefixTitle(tag: SNTag): string | undefined + getNoteLinkedFiles(note: SNNote): FileItem[] + conflictsOf(uuid: string): AnyItemInterface[] } diff --git a/packages/services/src/Domain/Item/ItemsClientInterface.ts b/packages/services/src/Domain/Item/ItemsClientInterface.ts deleted file mode 100644 index 38add2623..000000000 --- a/packages/services/src/Domain/Item/ItemsClientInterface.ts +++ /dev/null @@ -1,174 +0,0 @@ -/* istanbul ignore file */ - -import { ContentType } from '@standardnotes/common' -import { - SNNote, - FileItem, - SNTag, - SmartView, - TagItemCountChangeObserver, - DecryptedPayloadInterface, - EncryptedItemInterface, - DecryptedTransferPayload, - PredicateInterface, - DecryptedItemInterface, - SNComponent, - SNTheme, - DisplayOptions, - ItemsKeyInterface, - ItemContent, - DecryptedPayload, - AnyItemInterface, -} from '@standardnotes/models' - -export interface ItemsClientInterface { - get invalidItems(): EncryptedItemInterface[] - - associateFileWithNote(file: FileItem, note: SNNote): Promise - - disassociateFileWithNote(file: FileItem, note: SNNote): Promise - - renameFile(file: FileItem, name: string): Promise - - addTagToNote(note: SNNote, tag: SNTag, addHierarchy: boolean): Promise - - addTagToFile(file: FileItem, tag: SNTag, addHierarchy: boolean): Promise - - /** Creates an unmanaged, un-inserted item from a payload. */ - createItemFromPayload(payload: DecryptedPayloadInterface): DecryptedItemInterface - - createPayloadFromObject(object: DecryptedTransferPayload): DecryptedPayloadInterface - - createTemplateItem< - C extends ItemContent = ItemContent, - I extends DecryptedItemInterface = DecryptedItemInterface, - >( - contentType: ContentType, - content?: C, - override?: Partial>, - ): I - - get trashedItems(): SNNote[] - - setPrimaryItemDisplayOptions(options: DisplayOptions): void - - getDisplayableNotes(): SNNote[] - - getDisplayableTags(): SNTag[] - - getDisplayableItemsKeys(): ItemsKeyInterface[] - - getDisplayableFiles(): FileItem[] - - getDisplayableNotesAndFiles(): (SNNote | FileItem)[] - - getDisplayableComponents(): (SNComponent | SNTheme)[] - - getItems(contentType: ContentType | ContentType[]): T[] - - insertItem(item: DecryptedItemInterface): Promise - - notesMatchingSmartView(view: SmartView): SNNote[] - - addNoteCountChangeObserver(observer: TagItemCountChangeObserver): () => void - - allCountableNotesCount(): number - allCountableFilesCount(): number - - countableNotesForTag(tag: SNTag | SmartView): number - - findTagByTitle(title: string): SNTag | undefined - - getTagPrefixTitle(tag: SNTag): string | undefined - - getTagLongTitle(tag: SNTag): string - - hasTagsNeedingFoldersMigration(): boolean - - referencesForItem(itemToLookupUuidFor: DecryptedItemInterface, contentType?: ContentType): DecryptedItemInterface[] - - itemsReferencingItem(itemToLookupUuidFor: DecryptedItemInterface, contentType?: ContentType): DecryptedItemInterface[] - - linkNoteToNote(note: SNNote, otherNote: SNNote): Promise - linkFileToFile(file: FileItem, otherFile: FileItem): Promise - - unlinkItems( - itemOne: DecryptedItemInterface, - itemTwo: DecryptedItemInterface, - ): Promise> - - /** - * Finds tags with title or component starting with a search query and (optionally) not associated with a note - * @param searchQuery - The query string to match - * @param note - The note whose tags should be omitted from results - * @returns Array containing tags matching search query and not associated with note - */ - searchTags(searchQuery: string, note?: SNNote): SNTag[] - - isValidTagParent(parentTagToLookUpUuidFor: SNTag, childToLookUpUuidFor: SNTag): boolean - - /** - * Returns the parent for a tag - */ - getTagParent(itemToLookupUuidFor: SNTag): SNTag | undefined - - /** - * Returns the hierarchy of parents for a tag - * @returns Array containing all parent tags - */ - getTagParentChain(itemToLookupUuidFor: SNTag): SNTag[] - - /** - * Returns all descendants for a tag - * @returns Array containing all descendant tags - */ - getTagChildren(itemToLookupUuidFor: SNTag): SNTag[] - - /** - * Get tags for a note sorted in natural order - * @param item - The item whose tags will be returned - * @returns Array containing tags associated with an item - */ - getSortedTagsForItem(item: DecryptedItemInterface): SNTag[] - - isSmartViewTitle(title: string): boolean - - getSmartViews(): SmartView[] - - getNoteCount(): number - - /** - * Finds an item by UUID. - */ - findItem(uuid: string): T | undefined - - /** - * Finds an item by predicate. - */ - findItems(uuids: string[]): T[] - - findSureItem(uuid: string): T - - /** - * Finds an item by predicate. - */ - itemsMatchingPredicate( - contentType: ContentType, - predicate: PredicateInterface, - ): T[] - - /** - * @param item item to be checked - * @returns Whether the item is a template (unmanaged) - */ - isTemplateItem(item: DecryptedItemInterface): boolean - - createSmartView>( - title: string, - predicate: PredicateInterface, - iconString?: string, - ): Promise - - conflictsOf(uuid: string): AnyItemInterface[] - numberOfNotesWithConflicts(): number -} diff --git a/packages/services/src/Domain/Item/ItemCounter.spec.ts b/packages/services/src/Domain/Item/StaticItemCounter.spec.ts similarity index 86% rename from packages/services/src/Domain/Item/ItemCounter.spec.ts rename to packages/services/src/Domain/Item/StaticItemCounter.spec.ts index ac00acc44..f64536991 100644 --- a/packages/services/src/Domain/Item/ItemCounter.spec.ts +++ b/packages/services/src/Domain/Item/StaticItemCounter.spec.ts @@ -1,9 +1,9 @@ import { ContentType } from '@standardnotes/common' import { SNNote, SNTag } from '@standardnotes/models' -import { ItemCounter } from './ItemCounter' +import { StaticItemCounter } from './StaticItemCounter' describe('ItemCounter', () => { - const createCounter = () => new ItemCounter() + const createCounter = () => new StaticItemCounter() it('should count distinct item counts', () => { const items = [ diff --git a/packages/services/src/Domain/Item/ItemCounter.ts b/packages/services/src/Domain/Item/StaticItemCounter.ts similarity index 85% rename from packages/services/src/Domain/Item/ItemCounter.ts rename to packages/services/src/Domain/Item/StaticItemCounter.ts index 6b678cfae..d8721d12d 100644 --- a/packages/services/src/Domain/Item/ItemCounter.ts +++ b/packages/services/src/Domain/Item/StaticItemCounter.ts @@ -1,9 +1,7 @@ import { ContentType } from '@standardnotes/common' import { SNNote, SNTag, ItemCounts } from '@standardnotes/models' -import { ItemCounterInterface } from './ItemCounterInterface' - -export class ItemCounter implements ItemCounterInterface { +export class StaticItemCounter { countNotesAndTags(items: Array): ItemCounts { const counts: ItemCounts = { notes: 0, diff --git a/packages/services/src/Domain/KeySystem/KeySystemKeyManager.ts b/packages/services/src/Domain/KeySystem/KeySystemKeyManager.ts new file mode 100644 index 000000000..e6b737c43 --- /dev/null +++ b/packages/services/src/Domain/KeySystem/KeySystemKeyManager.ts @@ -0,0 +1,158 @@ +import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' +import { ApplicationStage } from './../Application/ApplicationStage' +import { InternalEventBusInterface } from './../Internal/InternalEventBusInterface' +import { StorageServiceInterface } from './../Storage/StorageServiceInterface' +import { + DecryptedPayload, + DecryptedTransferPayload, + EncryptedItemInterface, + KeySystemIdentifier, + KeySystemItemsKeyInterface, + KeySystemRootKey, + KeySystemRootKeyContent, + KeySystemRootKeyInterface, + KeySystemRootKeyStorageMode, + Predicate, + VaultListingInterface, +} from '@standardnotes/models' +import { ItemManagerInterface } from './../Item/ItemManagerInterface' +import { ContentType } from '@standardnotes/common' +import { KeySystemKeyManagerInterface } from '@standardnotes/encryption' +import { AbstractService } from '../Service/AbstractService' + +const RootKeyStorageKeyPrefix = 'key-system-root-key-' + +export class KeySystemKeyManager extends AbstractService implements KeySystemKeyManagerInterface { + private rootKeyMemoryCache: Record = {} + + constructor( + private readonly items: ItemManagerInterface, + private readonly mutator: MutatorClientInterface, + private readonly storage: StorageServiceInterface, + eventBus: InternalEventBusInterface, + ) { + super(eventBus) + } + + public override async handleApplicationStage(stage: ApplicationStage): Promise { + if (stage === ApplicationStage.StorageDecrypted_09) { + this.loadRootKeysFromStorage() + } + } + + private loadRootKeysFromStorage(): void { + const storageKeys = this.storage.getAllKeys().filter((key) => key.startsWith(RootKeyStorageKeyPrefix)) + + const keyRawPayloads = storageKeys.map((key) => + this.storage.getValue>(key), + ) + + const keyPayloads = keyRawPayloads.map((rawPayload) => new DecryptedPayload(rawPayload)) + + const keys = keyPayloads.map((payload) => new KeySystemRootKey(payload)) + keys.forEach((key) => { + this.rootKeyMemoryCache[key.systemIdentifier] = key + }) + } + + private storageKeyForRootKey(systemIdentifier: KeySystemIdentifier): string { + return `${RootKeyStorageKeyPrefix}${systemIdentifier}` + } + + public intakeNonPersistentKeySystemRootKey( + key: KeySystemRootKeyInterface, + storage: KeySystemRootKeyStorageMode, + ): void { + this.rootKeyMemoryCache[key.systemIdentifier] = key + + if (storage === KeySystemRootKeyStorageMode.Local) { + this.storage.setValue(this.storageKeyForRootKey(key.systemIdentifier), key.payload.ejected()) + } + } + + public undoIntakeNonPersistentKeySystemRootKey(systemIdentifier: KeySystemIdentifier): void { + delete this.rootKeyMemoryCache[systemIdentifier] + void this.storage.removeValue(this.storageKeyForRootKey(systemIdentifier)) + } + + public getAllSyncedKeySystemRootKeys(): KeySystemRootKeyInterface[] { + return this.items.getItems(ContentType.KeySystemRootKey) + } + + public clearMemoryOfKeysRelatedToVault(vault: VaultListingInterface): void { + delete this.rootKeyMemoryCache[vault.systemIdentifier] + + const itemsKeys = this.getKeySystemItemsKeys(vault.systemIdentifier) + this.items.removeItemsLocally(itemsKeys) + } + + public getSyncedKeySystemRootKeysForVault(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface[] { + return this.items.itemsMatchingPredicate( + ContentType.KeySystemRootKey, + new Predicate('systemIdentifier', '=', systemIdentifier), + ) + } + + public getAllKeySystemRootKeysForVault(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface[] { + const synced = this.getSyncedKeySystemRootKeysForVault(systemIdentifier) + const memory = this.rootKeyMemoryCache[systemIdentifier] ? [this.rootKeyMemoryCache[systemIdentifier]] : [] + return [...synced, ...memory] + } + + public async deleteNonPersistentSystemRootKeysForVault(systemIdentifier: KeySystemIdentifier): Promise { + delete this.rootKeyMemoryCache[systemIdentifier] + + await this.storage.removeValue(this.storageKeyForRootKey(systemIdentifier)) + } + + public async deleteAllSyncedKeySystemRootKeys(systemIdentifier: KeySystemIdentifier): Promise { + const keys = this.getSyncedKeySystemRootKeysForVault(systemIdentifier) + await this.mutator.setItemsToBeDeleted(keys) + } + + public getKeySystemRootKeyWithToken( + systemIdentifier: KeySystemIdentifier, + rootKeyToken: string, + ): KeySystemRootKeyInterface | undefined { + const keys = this.getAllKeySystemRootKeysForVault(systemIdentifier).filter((key) => key.token === rootKeyToken) + + if (keys.length > 1) { + throw new Error('Multiple synced key system root keys found for token') + } + + return keys[0] + } + + public getPrimaryKeySystemRootKey(systemIdentifier: KeySystemIdentifier): KeySystemRootKeyInterface | undefined { + const keys = this.getAllKeySystemRootKeysForVault(systemIdentifier) + + const sortedByNewestFirst = keys.sort((a, b) => b.keyParams.creationTimestamp - a.keyParams.creationTimestamp) + return sortedByNewestFirst[0] + } + + public getAllKeySystemItemsKeys(): (KeySystemItemsKeyInterface | EncryptedItemInterface)[] { + const decryptedItems = this.items.getItems(ContentType.KeySystemItemsKey) + const encryptedItems = this.items.invalidItems.filter((item) => item.content_type === ContentType.KeySystemItemsKey) + return [...decryptedItems, ...encryptedItems] + } + + public getKeySystemItemsKeys(systemIdentifier: KeySystemIdentifier): KeySystemItemsKeyInterface[] { + return this.items + .getItems(ContentType.KeySystemItemsKey) + .filter((key) => key.key_system_identifier === systemIdentifier) + } + + public getPrimaryKeySystemItemsKey(systemIdentifier: KeySystemIdentifier): KeySystemItemsKeyInterface { + const rootKey = this.getPrimaryKeySystemRootKey(systemIdentifier) + if (!rootKey) { + throw new Error('No primary key system root key found') + } + + const matchingItemsKeys = this.getKeySystemItemsKeys(systemIdentifier).filter( + (key) => key.rootKeyToken === rootKey.token, + ) + + const sortedByNewestFirst = matchingItemsKeys.sort((a, b) => b.creationTimestamp - a.creationTimestamp) + return sortedByNewestFirst[0] + } +} diff --git a/packages/services/src/Domain/Mutator/ImportDataUseCase.ts b/packages/services/src/Domain/Mutator/ImportDataUseCase.ts new file mode 100644 index 000000000..6812c71bb --- /dev/null +++ b/packages/services/src/Domain/Mutator/ImportDataUseCase.ts @@ -0,0 +1,146 @@ +import { HistoryServiceInterface } from '../History/HistoryServiceInterface' +import { ChallengeServiceInterface } from '../Challenge/ChallengeServiceInterface' +import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface' +import { ProtectionsClientInterface } from '../Protection/ProtectionClientInterface' +import { SyncServiceInterface } from '../Sync/SyncServiceInterface' +import { ItemManagerInterface } from '../Item/ItemManagerInterface' +import { ContentType, ProtocolVersion, compareVersions } from '@standardnotes/common' +import { + BackupFile, + BackupFileDecryptedContextualPayload, + ComponentContent, + CopyPayloadWithContentOverride, + CreateDecryptedBackupFileContextPayload, + CreateEncryptedBackupFileContextPayload, + DecryptedItemInterface, + DecryptedPayloadInterface, + isDecryptedPayload, + isEncryptedTransferPayload, +} from '@standardnotes/models' +import { ClientDisplayableError } from '@standardnotes/responses' +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { Challenge, ChallengePrompt, ChallengeReason, ChallengeValidation } from '../Challenge' + +const Strings = { + UnsupportedBackupFileVersion: + 'This backup file was created using a newer version of the application and cannot be imported here. Please update your application and try again.', + BackupFileMoreRecentThanAccount: + "This backup file was created using a newer encryption version than your account's. Please run the available encryption upgrade and try again.", + FileAccountPassword: 'File account password', +} + +export type ImportDataReturnType = + | { + affectedItems: DecryptedItemInterface[] + errorCount: number + } + | { + error: ClientDisplayableError + } + +export class ImportDataUseCase { + constructor( + private itemManager: ItemManagerInterface, + private syncService: SyncServiceInterface, + private protectionService: ProtectionsClientInterface, + private encryption: EncryptionProviderInterface, + private payloadManager: PayloadManagerInterface, + private challengeService: ChallengeServiceInterface, + private historyService: HistoryServiceInterface, + ) {} + + /** + * @returns + * .affectedItems: Items that were either created or dirtied by this import + * .errorCount: The number of items that were not imported due to failure to decrypt. + */ + + async execute(data: BackupFile, awaitSync = false): Promise { + if (data.version) { + /** + * Prior to 003 backup files did not have a version field so we cannot + * stop importing if there is no backup file version, only if there is + * an unsupported version. + */ + const version = data.version as ProtocolVersion + + const supportedVersions = this.encryption.supportedVersions() + if (!supportedVersions.includes(version)) { + return { error: new ClientDisplayableError(Strings.UnsupportedBackupFileVersion) } + } + + const userVersion = this.encryption.getUserVersion() + if (userVersion && compareVersions(version, userVersion) === 1) { + /** File was made with a greater version than the user's account */ + return { error: new ClientDisplayableError(Strings.BackupFileMoreRecentThanAccount) } + } + } + + let password: string | undefined + + if (data.auth_params || data.keyParams) { + /** Get import file password. */ + const challenge = new Challenge( + [new ChallengePrompt(ChallengeValidation.None, Strings.FileAccountPassword, undefined, true)], + ChallengeReason.DecryptEncryptedFile, + true, + ) + const passwordResponse = await this.challengeService.promptForChallengeResponse(challenge) + if (passwordResponse == undefined) { + /** Challenge was canceled */ + return { error: new ClientDisplayableError('Import aborted') } + } + this.challengeService.completeChallenge(challenge) + password = passwordResponse?.values[0].value as string + } + + if (!(await this.protectionService.authorizeFileImport())) { + return { error: new ClientDisplayableError('Import aborted') } + } + + data.items = data.items.map((item) => { + if (isEncryptedTransferPayload(item)) { + return CreateEncryptedBackupFileContextPayload(item) + } else { + return CreateDecryptedBackupFileContextPayload(item as BackupFileDecryptedContextualPayload) + } + }) + + const decryptedPayloadsOrError = await this.encryption.decryptBackupFile(data, password) + + if (decryptedPayloadsOrError instanceof ClientDisplayableError) { + return { error: decryptedPayloadsOrError } + } + + const validPayloads = decryptedPayloadsOrError.filter(isDecryptedPayload).map((payload) => { + /* Don't want to activate any components during import process in + * case of exceptions breaking up the import proccess */ + if (payload.content_type === ContentType.Component && (payload.content as ComponentContent).active) { + const typedContent = payload as DecryptedPayloadInterface + return CopyPayloadWithContentOverride(typedContent, { + active: false, + }) + } else { + return payload + } + }) + + const affectedUuids = await this.payloadManager.importPayloads( + validPayloads, + this.historyService.getHistoryMapCopy(), + ) + + const promise = this.syncService.sync() + + if (awaitSync) { + await promise + } + + const affectedItems = this.itemManager.findItems(affectedUuids) as DecryptedItemInterface[] + + return { + affectedItems: affectedItems, + errorCount: decryptedPayloadsOrError.length - validPayloads.length, + } + } +} diff --git a/packages/services/src/Domain/Mutator/MutatorClientInterface.ts b/packages/services/src/Domain/Mutator/MutatorClientInterface.ts index b8626ad17..131081277 100644 --- a/packages/services/src/Domain/Mutator/MutatorClientInterface.ts +++ b/packages/services/src/Domain/Mutator/MutatorClientInterface.ts @@ -1,70 +1,92 @@ import { ContentType } from '@standardnotes/common' import { - BackupFile, + ComponentMutator, DecryptedItemInterface, DecryptedItemMutator, - DecryptedPayload, + DecryptedPayloadInterface, EncryptedItemInterface, + FeatureRepoMutator, FileItem, ItemContent, + ItemsKeyInterface, + ItemsKeyMutatorInterface, + MutationType, PayloadEmitSource, + PredicateInterface, SmartView, SNComponent, + SNFeatureRepo, SNNote, SNTag, TransactionalMutation, + VaultListingInterface, } from '@standardnotes/models' -import { ClientDisplayableError } from '@standardnotes/responses' - -import { ChallengeReason } from '../Challenge/Types/ChallengeReason' -import { SyncOptions } from '../Sync/SyncOptions' export interface MutatorClientInterface { /** * Inserts the input item by its payload properties, and marks the item as dirty. * A sync is not performed after an item is inserted. This must be handled by the caller. */ - insertItem(item: DecryptedItemInterface): Promise + insertItem(item: DecryptedItemInterface, setDirty?: boolean): Promise + emitItemFromPayload(payload: DecryptedPayloadInterface, source: PayloadEmitSource): Promise - /** - * Mutates a pre-existing item, marks it as dirty, and syncs it - */ - changeAndSaveItem( - itemToLookupUuidFor: DecryptedItemInterface, - mutate: (mutator: M) => void, - updateTimestamps?: boolean, - emitSource?: PayloadEmitSource, - syncOptions?: SyncOptions, - ): Promise - - /** - * Mutates pre-existing items, marks them as dirty, and syncs - */ - changeAndSaveItems( + setItemToBeDeleted(itemToLookupUuidFor: DecryptedItemInterface, source?: PayloadEmitSource): Promise + setItemsToBeDeleted(itemsToLookupUuidsFor: DecryptedItemInterface[]): Promise + setItemsDirty( itemsToLookupUuidsFor: DecryptedItemInterface[], - mutate: (mutator: M) => void, - updateTimestamps?: boolean, + isUserModified?: boolean, + ): Promise + createItem( + contentType: ContentType, + content: C, + needsSync?: boolean, + vault?: VaultListingInterface, + ): Promise + + changeItem< + M extends DecryptedItemMutator = DecryptedItemMutator, + I extends DecryptedItemInterface = DecryptedItemInterface, + >( + itemToLookupUuidFor: I, + mutate?: (mutator: M) => void, + mutationType?: MutationType, emitSource?: PayloadEmitSource, - syncOptions?: SyncOptions, - ): Promise + payloadSourceKey?: string, + ): Promise + changeItems< + M extends DecryptedItemMutator = DecryptedItemMutator, + I extends DecryptedItemInterface = DecryptedItemInterface, + >( + itemsToLookupUuidsFor: I[], + mutate?: (mutator: M) => void, + mutationType?: MutationType, + emitSource?: PayloadEmitSource, + payloadSourceKey?: string, + ): Promise - /** - * Mutates a pre-existing item and marks it as dirty. Does not sync changes. - */ - changeItem( - itemToLookupUuidFor: DecryptedItemInterface, - mutate: (mutator: M) => void, - updateTimestamps?: boolean, - ): Promise + changeItemsKey( + itemToLookupUuidFor: ItemsKeyInterface, + mutate: (mutator: ItemsKeyMutatorInterface) => void, + mutationType?: MutationType, + emitSource?: PayloadEmitSource, + payloadSourceKey?: string, + ): Promise - /** - * Mutates a pre-existing items and marks them as dirty. Does not sync changes. - */ - changeItems( - itemsToLookupUuidsFor: DecryptedItemInterface[], - mutate: (mutator: M) => void, - updateTimestamps?: boolean, - ): Promise<(DecryptedItemInterface | undefined)[]> + changeComponent( + itemToLookupUuidFor: SNComponent, + mutate: (mutator: ComponentMutator) => void, + mutationType?: MutationType, + emitSource?: PayloadEmitSource, + payloadSourceKey?: string, + ): Promise + + changeFeatureRepo( + itemToLookupUuidFor: SNFeatureRepo, + mutate: (mutator: FeatureRepoMutator) => void, + mutationType?: MutationType, + emitSource?: PayloadEmitSource, + payloadSourceKey?: string, + ): Promise /** * Run unique mutations per each item in the array, then only propagate all changes @@ -83,44 +105,11 @@ export interface MutatorClientInterface { payloadSourceKey?: string, ): Promise - protectItems<_M extends DecryptedItemMutator, I extends DecryptedItemInterface>( - items: I[], - ): Promise - - unprotectItems<_M extends DecryptedItemMutator, I extends DecryptedItemInterface>( - items: I[], - reason: ChallengeReason, - ): Promise - - protectNote(note: SNNote): Promise - - unprotectNote(note: SNNote): Promise - - protectNotes(notes: SNNote[]): Promise - - unprotectNotes(notes: SNNote[]): Promise - - protectFile(file: FileItem): Promise - - unprotectFile(file: FileItem): Promise - /** * Takes the values of the input item and emits it onto global state. */ mergeItem(item: DecryptedItemInterface, source: PayloadEmitSource): Promise - /** - * Creates an unmanaged item that can be added later. - */ - createTemplateItem< - C extends ItemContent = ItemContent, - I extends DecryptedItemInterface = DecryptedItemInterface, - >( - contentType: ContentType, - content?: C, - override?: Partial>, - ): I - /** * @param isUserModified Whether to change the modified date the user * sees of the item. @@ -135,7 +124,13 @@ export interface MutatorClientInterface { emptyTrash(): Promise - duplicateItem(item: T, additionalContent?: Partial): Promise + duplicateItem( + itemToLookupUuidFor: T, + isConflict?: boolean, + additionalContent?: Partial, + ): Promise + + addTagToNote(note: SNNote, tag: SNTag, addHierarchy: boolean): Promise /** * Migrates any tags containing a '.' character to sa chema-based heirarchy, removing @@ -146,41 +141,35 @@ export interface MutatorClientInterface { /** * Establishes a hierarchical relationship between two tags. */ - setTagParent(parentTag: SNTag, childTag: SNTag): Promise + setTagParent(parentTag: SNTag, childTag: SNTag): Promise /** * Remove the tag parent. */ - unsetTagParent(childTag: SNTag): Promise + unsetTagParent(childTag: SNTag): Promise - findOrCreateTag(title: string): Promise + findOrCreateTag(title: string, createInVault?: VaultListingInterface): Promise /** Creates and returns the tag but does not run sync. Callers must perform sync. */ - createTagOrSmartView(title: string): Promise + createTagOrSmartView(title: string, vault?: VaultListingInterface): Promise + findOrCreateTagParentChain(titlesHierarchy: string[]): Promise - /** - * Activates or deactivates a component, depending on its - * current state, and syncs. - */ - toggleComponent(component: SNComponent): Promise + associateFileWithNote(file: FileItem, note: SNNote): Promise - toggleTheme(theme: SNComponent): Promise + disassociateFileWithNote(file: FileItem, note: SNNote): Promise + renameFile(file: FileItem, name: string): Promise - /** - * @returns - * .affectedItems: Items that were either created or dirtied by this import - * .errorCount: The number of items that were not imported due to failure to decrypt. - */ - importData( - data: BackupFile, - awaitSync?: boolean, - ): Promise< - | { - affectedItems: DecryptedItemInterface[] - errorCount: number - } - | { - error: ClientDisplayableError - } - > + unlinkItems( + itemA: DecryptedItemInterface, + itemB: DecryptedItemInterface, + ): Promise> + createSmartView(dto: { + title: string + predicate: PredicateInterface + iconString?: string + vault?: VaultListingInterface + }): Promise + linkNoteToNote(note: SNNote, otherNote: SNNote): Promise + linkFileToFile(file: FileItem, otherFile: FileItem): Promise + addTagToFile(file: FileItem, tag: SNTag, addHierarchy: boolean): Promise } diff --git a/packages/services/src/Domain/Payloads/PayloadManagerInterface.ts b/packages/services/src/Domain/Payloads/PayloadManagerInterface.ts index d72ce4d06..24b8e2245 100644 --- a/packages/services/src/Domain/Payloads/PayloadManagerInterface.ts +++ b/packages/services/src/Domain/Payloads/PayloadManagerInterface.ts @@ -25,4 +25,6 @@ export interface PayloadManagerInterface { get nonDeletedItems(): FullyFormedPayloadInterface[] importPayloads(payloads: DecryptedPayloadInterface[], historyMap: HistoryMap): Promise + + removePayloadLocally(payload: FullyFormedPayloadInterface | FullyFormedPayloadInterface[]): void } diff --git a/packages/services/src/Domain/Protection/ProtectionClientInterface.ts b/packages/services/src/Domain/Protection/ProtectionClientInterface.ts index a6f5fd1c0..2be79c934 100644 --- a/packages/services/src/Domain/Protection/ProtectionClientInterface.ts +++ b/packages/services/src/Domain/Protection/ProtectionClientInterface.ts @@ -1,4 +1,4 @@ -import { DecryptedItem } from '@standardnotes/models' +import { DecryptedItem, DecryptedItemInterface, FileItem, SNNote } from '@standardnotes/models' import { ChallengeReason } from '../Challenge' import { MobileUnlockTiming } from './MobileUnlockTiming' import { TimingDisplayOption } from './TimingDisplayOption' @@ -24,4 +24,13 @@ export interface ProtectionsClientInterface { authorizeAddingPasscode(): Promise authorizeRemovingPasscode(): Promise authorizeChangingPasscode(): Promise + authorizeFileImport(): Promise + protectItems(items: I[]): Promise + unprotectItems(items: I[], reason: ChallengeReason): Promise + protectNote(note: SNNote): Promise + unprotectNote(note: SNNote): Promise + protectNotes(notes: SNNote[]): Promise + unprotectNotes(notes: SNNote[]): Promise + protectFile(file: FileItem): Promise + unprotectFile(file: FileItem): Promise } diff --git a/packages/services/src/Domain/Revision/RevisionClientInterface.ts b/packages/services/src/Domain/Revision/RevisionClientInterface.ts index 95d446cc5..21b6b19bf 100644 --- a/packages/services/src/Domain/Revision/RevisionClientInterface.ts +++ b/packages/services/src/Domain/Revision/RevisionClientInterface.ts @@ -1,4 +1,5 @@ import { Uuid } from '@standardnotes/domain-core' +import { RevisionPayload } from './RevisionPayload' export interface RevisionClientInterface { listRevisions(itemUuid: Uuid): Promise< @@ -11,18 +12,5 @@ export interface RevisionClientInterface { }> > deleteRevision(itemUuid: Uuid, revisionUuid: Uuid): Promise - getRevision( - itemUuid: Uuid, - revisionUuid: Uuid, - ): Promise<{ - uuid: string - item_uuid: string - content: string | null - content_type: string - items_key_id: string | null - enc_item_key: string | null - auth_hash: string | null - created_at: string - updated_at: string - } | null> + getRevision(itemUuid: Uuid, revisionUuid: Uuid): Promise } diff --git a/packages/services/src/Domain/Revision/RevisionManager.ts b/packages/services/src/Domain/Revision/RevisionManager.ts index a3b174dc3..5b6dbd9b0 100644 --- a/packages/services/src/Domain/Revision/RevisionManager.ts +++ b/packages/services/src/Domain/Revision/RevisionManager.ts @@ -5,6 +5,7 @@ import { isErrorResponse } from '@standardnotes/responses' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { AbstractService } from '../Service/AbstractService' import { RevisionClientInterface } from './RevisionClientInterface' +import { RevisionPayload } from './RevisionPayload' export class RevisionManager extends AbstractService implements RevisionClientInterface { constructor( @@ -36,20 +37,7 @@ export class RevisionManager extends AbstractService implements RevisionClientIn return result.data.message } - async getRevision( - itemUuid: Uuid, - revisionUuid: Uuid, - ): Promise<{ - uuid: string - item_uuid: string - content: string | null - content_type: string - items_key_id: string | null - enc_item_key: string | null - auth_hash: string | null - created_at: string - updated_at: string - } | null> { + async getRevision(itemUuid: Uuid, revisionUuid: Uuid): Promise { const result = await this.revisionApiService.getRevision(itemUuid.value, revisionUuid.value) if (isErrorResponse(result)) { diff --git a/packages/services/src/Domain/Revision/RevisionPayload.ts b/packages/services/src/Domain/Revision/RevisionPayload.ts new file mode 100644 index 000000000..39140cb1f --- /dev/null +++ b/packages/services/src/Domain/Revision/RevisionPayload.ts @@ -0,0 +1,14 @@ +export type RevisionPayload = { + uuid: string + item_uuid: string + content: string | null + content_type: string + items_key_id: string | null + enc_item_key: string | null + auth_hash: string | null + created_at: string + updated_at: string + user_uuid: string + key_system_identifier: string | null + shared_vault_uuid: string | null +} diff --git a/packages/services/src/Domain/Service/AbstractService.ts b/packages/services/src/Domain/Service/AbstractService.ts index 47448a0f3..a6c0aa7f3 100644 --- a/packages/services/src/Domain/Service/AbstractService.ts +++ b/packages/services/src/Domain/Service/AbstractService.ts @@ -8,13 +8,15 @@ import { ApplicationStage } from '../Application/ApplicationStage' import { InternalEventPublishStrategy } from '../Internal/InternalEventPublishStrategy' import { DiagnosticInfo } from '../Diagnostics/ServiceDiagnostics' -export abstract class AbstractService +export abstract class AbstractService implements ServiceInterface { private eventObservers: EventObserver[] = [] public loggingEnabled = false private criticalPromises: Promise[] = [] + protected eventDisposers: (() => void)[] = [] + constructor(protected internalEventBus: InternalEventBusInterface) {} public addEventObserver(observer: EventObserver): () => void { @@ -71,6 +73,11 @@ export abstract class AbstractService this.eventObservers.length = 0 ;(this.internalEventBus as unknown) = undefined ;(this.criticalPromises as unknown) = undefined + + for (const disposer of this.eventDisposers) { + disposer() + } + this.eventDisposers = [] } /** diff --git a/packages/services/src/Domain/Session/SessionEvent.ts b/packages/services/src/Domain/Session/SessionEvent.ts new file mode 100644 index 000000000..aa5ad855b --- /dev/null +++ b/packages/services/src/Domain/Session/SessionEvent.ts @@ -0,0 +1,5 @@ +export enum SessionEvent { + Restored = 'SessionRestored', + Revoked = 'SessionRevoked', + UserKeyPairChanged = 'UserKeyPairChanged', +} diff --git a/packages/services/src/Domain/Session/SessionsClientInterface.ts b/packages/services/src/Domain/Session/SessionsClientInterface.ts index ef61a58a1..fb289bb6f 100644 --- a/packages/services/src/Domain/Session/SessionsClientInterface.ts +++ b/packages/services/src/Domain/Session/SessionsClientInterface.ts @@ -10,7 +10,11 @@ import { SessionManagerResponse } from './SessionManagerResponse' export interface SessionsClientInterface { getWorkspaceDisplayIdentifier(): string populateSessionFromDemoShareToken(token: Base64String): Promise + getUser(): User | undefined + get userUuid(): string + getSureUser(): User + isCurrentSessionReadOnly(): boolean | undefined register(email: string, password: string, ephemeral: boolean): Promise signIn( @@ -20,7 +24,7 @@ export interface SessionsClientInterface { ephemeral: boolean, minAllowedVersion?: ProtocolVersion, ): Promise - getSureUser(): User + isSignedIn(): boolean bypassChecksAndSignInWithRootKey( email: string, rootKey: RootKeyInterface, @@ -42,4 +46,8 @@ export interface SessionsClientInterface { rootKey: SNRootKey wrappingKey?: SNRootKey }): Promise + + getPublicKey(): string + getSigningPublicKey(): string + isUserMissingKeyPair(): boolean } diff --git a/packages/services/src/Domain/Session/UserKeyPairChangedEventData.ts b/packages/services/src/Domain/Session/UserKeyPairChangedEventData.ts new file mode 100644 index 000000000..9a56d6a20 --- /dev/null +++ b/packages/services/src/Domain/Session/UserKeyPairChangedEventData.ts @@ -0,0 +1,9 @@ +import { PkcKeyPair } from '@standardnotes/sncrypto-common' + +export type UserKeyPairChangedEventData = { + oldKeyPair: PkcKeyPair | undefined + oldSigningKeyPair: PkcKeyPair | undefined + + newKeyPair: PkcKeyPair + newSigningKeyPair: PkcKeyPair +} diff --git a/packages/services/src/Domain/SharedVaults/PendingSharedVaultInviteRecord.ts b/packages/services/src/Domain/SharedVaults/PendingSharedVaultInviteRecord.ts new file mode 100644 index 000000000..f2260571e --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/PendingSharedVaultInviteRecord.ts @@ -0,0 +1,8 @@ +import { AsymmetricMessageSharedVaultInvite } from '@standardnotes/models' +import { SharedVaultInviteServerHash } from '@standardnotes/responses' + +export type PendingSharedVaultInviteRecord = { + invite: SharedVaultInviteServerHash + message: AsymmetricMessageSharedVaultInvite + trusted: boolean +} diff --git a/packages/services/src/Domain/SharedVaults/SharedVaultService.ts b/packages/services/src/Domain/SharedVaults/SharedVaultService.ts new file mode 100644 index 000000000..ae3356ce7 --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/SharedVaultService.ts @@ -0,0 +1,587 @@ +import { MutatorClientInterface } from './../Mutator/MutatorClientInterface' +import { StorageServiceInterface } from './../Storage/StorageServiceInterface' +import { InviteContactToSharedVaultUseCase } from './UseCase/InviteContactToSharedVault' +import { + ClientDisplayableError, + SharedVaultInviteServerHash, + isErrorResponse, + SharedVaultUserServerHash, + isClientDisplayableError, + SharedVaultPermission, + UserEventType, +} from '@standardnotes/responses' +import { + HttpServiceInterface, + SharedVaultServerInterface, + SharedVaultUsersServerInterface, + SharedVaultInvitesServerInterface, + SharedVaultUsersServer, + SharedVaultInvitesServer, + SharedVaultServer, + AsymmetricMessageServerInterface, + AsymmetricMessageServer, +} from '@standardnotes/api' +import { + DecryptedItemInterface, + PayloadEmitSource, + TrustedContactInterface, + SharedVaultListingInterface, + VaultListingInterface, + AsymmetricMessageSharedVaultInvite, + KeySystemRootKeyStorageMode, +} from '@standardnotes/models' +import { SharedVaultServiceInterface } from './SharedVaultServiceInterface' +import { SharedVaultServiceEvent, SharedVaultServiceEventPayload } from './SharedVaultServiceEvent' +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { ContentType } from '@standardnotes/common' +import { GetSharedVaultUsersUseCase } from './UseCase/GetSharedVaultUsers' +import { RemoveVaultMemberUseCase } from './UseCase/RemoveSharedVaultMember' +import { AbstractService } from '../Service/AbstractService' +import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface' +import { SyncServiceInterface } from '../Sync/SyncServiceInterface' +import { ItemManagerInterface } from '../Item/ItemManagerInterface' +import { SessionsClientInterface } from '../Session/SessionsClientInterface' +import { ContactServiceInterface } from '../Contacts/ContactServiceInterface' +import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' +import { SyncEvent, SyncEventReceivedSharedVaultInvitesData } from '../Event/SyncEvent' +import { SessionEvent } from '../Session/SessionEvent' +import { InternalEventInterface } from '../Internal/InternalEventInterface' +import { FilesClientInterface } from '@standardnotes/files' +import { LeaveVaultUseCase } from './UseCase/LeaveSharedVault' +import { VaultServiceInterface } from '../Vaults/VaultServiceInterface' +import { UserEventServiceEvent, UserEventServiceEventPayload } from '../UserEvent/UserEventServiceEvent' +import { DeleteExternalSharedVaultUseCase } from './UseCase/DeleteExternalSharedVault' +import { DeleteSharedVaultUseCase } from './UseCase/DeleteSharedVault' +import { VaultServiceEvent, VaultServiceEventPayload } from '../Vaults/VaultServiceEvent' +import { AcceptTrustedSharedVaultInvite } from './UseCase/AcceptTrustedSharedVaultInvite' +import { GetAsymmetricMessageTrustedPayload } from '../AsymmetricMessage/UseCase/GetAsymmetricMessageTrustedPayload' +import { PendingSharedVaultInviteRecord } from './PendingSharedVaultInviteRecord' +import { GetAsymmetricMessageUntrustedPayload } from '../AsymmetricMessage/UseCase/GetAsymmetricMessageUntrustedPayload' +import { ShareContactWithAllMembersOfSharedVaultUseCase } from './UseCase/ShareContactWithAllMembersOfSharedVault' +import { GetSharedVaultTrustedContacts } from './UseCase/GetSharedVaultTrustedContacts' +import { NotifySharedVaultUsersOfRootKeyRotationUseCase } from './UseCase/NotifySharedVaultUsersOfRootKeyRotation' +import { CreateSharedVaultUseCase } from './UseCase/CreateSharedVault' +import { SendSharedVaultMetadataChangedMessageToAll } from './UseCase/SendSharedVaultMetadataChangedMessageToAll' +import { ConvertToSharedVaultUseCase } from './UseCase/ConvertToSharedVault' +import { GetVaultUseCase } from '../Vaults/UseCase/GetVault' + +export class SharedVaultService + extends AbstractService + implements SharedVaultServiceInterface, InternalEventHandlerInterface +{ + private server: SharedVaultServerInterface + private usersServer: SharedVaultUsersServerInterface + private invitesServer: SharedVaultInvitesServerInterface + private messageServer: AsymmetricMessageServerInterface + + private pendingInvites: Record = {} + + constructor( + http: HttpServiceInterface, + private sync: SyncServiceInterface, + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private encryption: EncryptionProviderInterface, + private session: SessionsClientInterface, + private contacts: ContactServiceInterface, + private files: FilesClientInterface, + private vaults: VaultServiceInterface, + private storage: StorageServiceInterface, + eventBus: InternalEventBusInterface, + ) { + super(eventBus) + + eventBus.addEventHandler(this, SessionEvent.UserKeyPairChanged) + eventBus.addEventHandler(this, UserEventServiceEvent.UserEventReceived) + eventBus.addEventHandler(this, VaultServiceEvent.VaultRootKeyRotated) + + this.server = new SharedVaultServer(http) + this.usersServer = new SharedVaultUsersServer(http) + this.invitesServer = new SharedVaultInvitesServer(http) + this.messageServer = new AsymmetricMessageServer(http) + + this.eventDisposers.push( + sync.addEventObserver(async (event, data) => { + if (event === SyncEvent.ReceivedSharedVaultInvites) { + void this.processInboundInvites(data as SyncEventReceivedSharedVaultInvitesData) + } else if (event === SyncEvent.ReceivedRemoteSharedVaults) { + void this.notifyCollaborationStatusChanged() + } + }), + ) + + this.eventDisposers.push( + items.addObserver(ContentType.TrustedContact, ({ changed, inserted, source }) => { + if (source === PayloadEmitSource.LocalChanged && inserted.length > 0) { + void this.handleCreationOfNewTrustedContacts(inserted) + } + if (source === PayloadEmitSource.LocalChanged && changed.length > 0) { + void this.handleTrustedContactsChange(changed) + } + }), + ) + + this.eventDisposers.push( + items.addObserver(ContentType.VaultListing, ({ changed, source }) => { + if (source === PayloadEmitSource.LocalChanged && changed.length > 0) { + void this.handleVaultListingsChange(changed) + } + }), + ) + } + + async handleEvent(event: InternalEventInterface): Promise { + if (event.type === SessionEvent.UserKeyPairChanged) { + void this.invitesServer.deleteAllInboundInvites() + } else if (event.type === UserEventServiceEvent.UserEventReceived) { + await this.handleUserEvent(event.payload as UserEventServiceEventPayload) + } else if (event.type === VaultServiceEvent.VaultRootKeyRotated) { + const payload = event.payload as VaultServiceEventPayload[VaultServiceEvent.VaultRootKeyRotated] + await this.handleVaultRootKeyRotatedEvent(payload.vault) + } + } + + private async handleUserEvent(event: UserEventServiceEventPayload): Promise { + if (event.eventPayload.eventType === UserEventType.RemovedFromSharedVault) { + const vault = new GetVaultUseCase(this.items).execute({ sharedVaultUuid: event.eventPayload.sharedVaultUuid }) + if (vault) { + const useCase = new DeleteExternalSharedVaultUseCase( + this.items, + this.mutator, + this.encryption, + this.storage, + this.sync, + ) + await useCase.execute(vault) + } + } else if (event.eventPayload.eventType === UserEventType.SharedVaultItemRemoved) { + const item = this.items.findItem(event.eventPayload.itemUuid) + if (item) { + this.items.removeItemsLocally([item]) + } + } + } + + private async handleVaultRootKeyRotatedEvent(vault: VaultListingInterface): Promise { + if (!vault.isSharedVaultListing()) { + return + } + + if (!this.isCurrentUserSharedVaultOwner(vault)) { + return + } + + const usecase = new NotifySharedVaultUsersOfRootKeyRotationUseCase( + this.usersServer, + this.invitesServer, + this.messageServer, + this.encryption, + this.contacts, + ) + + await usecase.execute({ sharedVault: vault, userUuid: this.session.getSureUser().uuid }) + } + + async createSharedVault(dto: { + name: string + description?: string + userInputtedPassword: string | undefined + storagePreference?: KeySystemRootKeyStorageMode + }): Promise { + const usecase = new CreateSharedVaultUseCase( + this.encryption, + this.items, + this.mutator, + this.sync, + this.files, + this.server, + ) + + return usecase.execute({ + vaultName: dto.name, + vaultDescription: dto.description, + userInputtedPassword: dto.userInputtedPassword, + storagePreference: dto.storagePreference ?? KeySystemRootKeyStorageMode.Synced, + }) + } + + async convertVaultToSharedVault( + vault: VaultListingInterface, + ): Promise { + const usecase = new ConvertToSharedVaultUseCase(this.items, this.mutator, this.sync, this.files, this.server) + + return usecase.execute({ vault }) + } + + public getCachedPendingInviteRecords(): PendingSharedVaultInviteRecord[] { + return Object.values(this.pendingInvites) + } + + private getAllSharedVaults(): SharedVaultListingInterface[] { + const vaults = this.vaults.getVaults().filter((vault) => vault.isSharedVaultListing()) + return vaults as SharedVaultListingInterface[] + } + + private findSharedVault(sharedVaultUuid: string): SharedVaultListingInterface | undefined { + return this.getAllSharedVaults().find((vault) => vault.sharing.sharedVaultUuid === sharedVaultUuid) + } + + public isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean { + if (!sharedVault.sharing.ownerUserUuid) { + throw new Error(`Shared vault ${sharedVault.sharing.sharedVaultUuid} does not have an owner user uuid`) + } + return sharedVault.sharing.ownerUserUuid === this.session.userUuid + } + + public isCurrentUserSharedVaultOwner(sharedVault: SharedVaultListingInterface): boolean { + if (!sharedVault.sharing.ownerUserUuid) { + throw new Error(`Shared vault ${sharedVault.sharing.sharedVaultUuid} does not have an owner user uuid`) + } + return sharedVault.sharing.ownerUserUuid === this.session.userUuid + } + + public isSharedVaultUserSharedVaultOwner(user: SharedVaultUserServerHash): boolean { + const vault = this.findSharedVault(user.shared_vault_uuid) + return vault != undefined && vault.sharing.ownerUserUuid === user.user_uuid + } + + private async handleCreationOfNewTrustedContacts(_contacts: TrustedContactInterface[]): Promise { + await this.downloadInboundInvites() + } + + private async handleTrustedContactsChange(contacts: TrustedContactInterface[]): Promise { + await this.reprocessCachedInvitesTrustStatusAfterTrustedContactsChange() + + for (const contact of contacts) { + await this.shareContactWithUserAdministeredSharedVaults(contact) + } + } + + private async handleVaultListingsChange(vaults: VaultListingInterface[]): Promise { + for (const vault of vaults) { + if (!vault.isSharedVaultListing()) { + continue + } + + const usecase = new SendSharedVaultMetadataChangedMessageToAll( + this.encryption, + this.contacts, + this.usersServer, + this.messageServer, + ) + + await usecase.execute({ + vault, + senderUuid: this.session.getSureUser().uuid, + senderEncryptionKeyPair: this.encryption.getKeyPair(), + senderSigningKeyPair: this.encryption.getSigningKeyPair(), + }) + } + } + + public async downloadInboundInvites(): Promise { + const response = await this.invitesServer.getInboundUserInvites() + + if (isErrorResponse(response)) { + return ClientDisplayableError.FromString(`Failed to get inbound user invites ${response}`) + } + + this.pendingInvites = {} + + await this.processInboundInvites(response.data.invites) + + return response.data.invites + } + + public async getOutboundInvites( + sharedVault?: SharedVaultListingInterface, + ): Promise { + const response = await this.invitesServer.getOutboundUserInvites() + + if (isErrorResponse(response)) { + return ClientDisplayableError.FromString(`Failed to get outbound user invites ${response}`) + } + + if (sharedVault) { + return response.data.invites.filter((invite) => invite.shared_vault_uuid === sharedVault.sharing.sharedVaultUuid) + } + + return response.data.invites + } + + public async deleteInvite(invite: SharedVaultInviteServerHash): Promise { + const response = await this.invitesServer.deleteInvite({ + sharedVaultUuid: invite.shared_vault_uuid, + inviteUuid: invite.uuid, + }) + + if (isErrorResponse(response)) { + return ClientDisplayableError.FromString(`Failed to delete invite ${response}`) + } + + delete this.pendingInvites[invite.uuid] + } + + public async deleteSharedVault(sharedVault: SharedVaultListingInterface): Promise { + const useCase = new DeleteSharedVaultUseCase(this.server, this.items, this.mutator, this.sync, this.encryption) + return useCase.execute({ sharedVault }) + } + + private async reprocessCachedInvitesTrustStatusAfterTrustedContactsChange(): Promise { + const cachedInvites = this.getCachedPendingInviteRecords() + + for (const record of cachedInvites) { + if (record.trusted) { + continue + } + + const trustedMessageUseCase = new GetAsymmetricMessageTrustedPayload( + this.encryption, + this.contacts, + ) + + const trustedMessage = trustedMessageUseCase.execute({ + message: record.invite, + privateKey: this.encryption.getKeyPair().privateKey, + }) + + if (trustedMessage) { + record.message = trustedMessage + record.trusted = true + } + } + } + + private async processInboundInvites(invites: SharedVaultInviteServerHash[]): Promise { + if (invites.length === 0) { + return + } + + for (const invite of invites) { + const trustedMessageUseCase = new GetAsymmetricMessageTrustedPayload( + this.encryption, + this.contacts, + ) + + const trustedMessage = trustedMessageUseCase.execute({ + message: invite, + privateKey: this.encryption.getKeyPair().privateKey, + }) + + if (trustedMessage) { + this.pendingInvites[invite.uuid] = { + invite, + message: trustedMessage, + trusted: true, + } + + continue + } + + const untrustedMessageUseCase = new GetAsymmetricMessageUntrustedPayload( + this.encryption, + ) + + const untrustedMessage = untrustedMessageUseCase.execute({ + message: invite, + privateKey: this.encryption.getKeyPair().privateKey, + }) + + if (untrustedMessage) { + this.pendingInvites[invite.uuid] = { + invite, + message: untrustedMessage, + trusted: false, + } + } + } + + await this.notifyCollaborationStatusChanged() + } + + private async notifyCollaborationStatusChanged(): Promise { + await this.notifyEventSync(SharedVaultServiceEvent.SharedVaultStatusChanged) + } + + async acceptPendingSharedVaultInvite(pendingInvite: PendingSharedVaultInviteRecord): Promise { + if (!pendingInvite.trusted) { + throw new Error('Cannot accept untrusted invite') + } + + const useCase = new AcceptTrustedSharedVaultInvite(this.invitesServer, this.mutator, this.sync, this.contacts) + await useCase.execute({ invite: pendingInvite.invite, message: pendingInvite.message }) + + delete this.pendingInvites[pendingInvite.invite.uuid] + + void this.sync.sync() + + await this.decryptErroredItemsAfterInviteAccept() + + await this.sync.syncSharedVaultsFromScratch([pendingInvite.invite.shared_vault_uuid]) + } + + private async decryptErroredItemsAfterInviteAccept(): Promise { + await this.encryption.decryptErroredPayloads() + } + + public async getInvitableContactsForSharedVault( + sharedVault: SharedVaultListingInterface, + ): Promise { + const users = await this.getSharedVaultUsers(sharedVault) + if (!users) { + return [] + } + + const contacts = this.contacts.getAllContacts() + return contacts.filter((contact) => { + const isContactAlreadyInVault = users.some((user) => user.user_uuid === contact.contactUuid) + return !isContactAlreadyInVault + }) + } + + private async getSharedVaultContacts(sharedVault: SharedVaultListingInterface): Promise { + const usecase = new GetSharedVaultTrustedContacts(this.contacts, this.usersServer) + const contacts = await usecase.execute(sharedVault) + if (!contacts) { + return [] + } + + return contacts + } + + async inviteContactToSharedVault( + sharedVault: SharedVaultListingInterface, + contact: TrustedContactInterface, + permissions: SharedVaultPermission, + ): Promise { + const sharedVaultContacts = await this.getSharedVaultContacts(sharedVault) + + const useCase = new InviteContactToSharedVaultUseCase(this.encryption, this.invitesServer) + + const result = await useCase.execute({ + senderKeyPair: this.encryption.getKeyPair(), + senderSigningKeyPair: this.encryption.getSigningKeyPair(), + sharedVault, + recipient: contact, + sharedVaultContacts, + permissions, + }) + + void this.notifyCollaborationStatusChanged() + + await this.sync.sync() + + return result + } + + async removeUserFromSharedVault( + sharedVault: SharedVaultListingInterface, + userUuid: string, + ): Promise { + if (!this.isCurrentUserSharedVaultAdmin(sharedVault)) { + throw new Error('Only vault admins can remove users') + } + + if (this.vaults.isVaultLocked(sharedVault)) { + throw new Error('Cannot remove user from locked vault') + } + + const useCase = new RemoveVaultMemberUseCase(this.usersServer) + const result = await useCase.execute({ sharedVaultUuid: sharedVault.sharing.sharedVaultUuid, userUuid }) + if (isClientDisplayableError(result)) { + return result + } + + void this.notifyCollaborationStatusChanged() + + await this.vaults.rotateVaultRootKey(sharedVault) + } + + async leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise { + const useCase = new LeaveVaultUseCase( + this.usersServer, + this.items, + this.mutator, + this.encryption, + this.storage, + this.sync, + ) + const result = await useCase.execute({ + sharedVault: sharedVault, + userUuid: this.session.getSureUser().uuid, + }) + + if (isClientDisplayableError(result)) { + return result + } + + void this.notifyCollaborationStatusChanged() + } + + async getSharedVaultUsers( + sharedVault: SharedVaultListingInterface, + ): Promise { + const useCase = new GetSharedVaultUsersUseCase(this.usersServer) + return useCase.execute({ sharedVaultUuid: sharedVault.sharing.sharedVaultUuid }) + } + + private async shareContactWithUserAdministeredSharedVaults(contact: TrustedContactInterface): Promise { + const sharedVaults = this.getAllSharedVaults() + + const useCase = new ShareContactWithAllMembersOfSharedVaultUseCase( + this.contacts, + this.encryption, + this.usersServer, + this.messageServer, + ) + + for (const vault of sharedVaults) { + if (!this.isCurrentUserSharedVaultAdmin(vault)) { + continue + } + + await useCase.execute({ + senderKeyPair: this.encryption.getKeyPair(), + senderSigningKeyPair: this.encryption.getSigningKeyPair(), + sharedVault: vault, + contactToShare: contact, + senderUserUuid: this.session.getSureUser().uuid, + }) + } + } + + getItemLastEditedBy(item: DecryptedItemInterface): TrustedContactInterface | undefined { + if (!item.last_edited_by_uuid) { + return undefined + } + + const contact = this.contacts.findTrustedContact(item.last_edited_by_uuid) + + return contact + } + + getItemSharedBy(item: DecryptedItemInterface): TrustedContactInterface | undefined { + if (!item.user_uuid || item.user_uuid === this.session.getSureUser().uuid) { + return undefined + } + + const contact = this.contacts.findTrustedContact(item.user_uuid) + + return contact + } + + override deinit(): void { + super.deinit() + ;(this.contacts as unknown) = undefined + ;(this.encryption as unknown) = undefined + ;(this.files as unknown) = undefined + ;(this.invitesServer as unknown) = undefined + ;(this.items as unknown) = undefined + ;(this.messageServer as unknown) = undefined + ;(this.server as unknown) = undefined + ;(this.session as unknown) = undefined + ;(this.sync as unknown) = undefined + ;(this.usersServer as unknown) = undefined + ;(this.vaults as unknown) = undefined + } +} diff --git a/packages/services/src/Domain/SharedVaults/SharedVaultServiceEvent.ts b/packages/services/src/Domain/SharedVaults/SharedVaultServiceEvent.ts new file mode 100644 index 000000000..ac210e91f --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/SharedVaultServiceEvent.ts @@ -0,0 +1,10 @@ +import { KeySystemIdentifier } from '@standardnotes/models' + +export enum SharedVaultServiceEvent { + SharedVaultStatusChanged = 'SharedVaultStatusChanged', +} + +export type SharedVaultServiceEventPayload = { + sharedVaultUuid: string + keySystemIdentifier: KeySystemIdentifier +} diff --git a/packages/services/src/Domain/SharedVaults/SharedVaultServiceInterface.ts b/packages/services/src/Domain/SharedVaults/SharedVaultServiceInterface.ts new file mode 100644 index 000000000..8de042513 --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/SharedVaultServiceInterface.ts @@ -0,0 +1,55 @@ +import { + ClientDisplayableError, + SharedVaultInviteServerHash, + SharedVaultUserServerHash, + SharedVaultPermission, +} from '@standardnotes/responses' +import { + DecryptedItemInterface, + TrustedContactInterface, + SharedVaultListingInterface, + VaultListingInterface, + KeySystemRootKeyStorageMode, +} from '@standardnotes/models' +import { AbstractService } from '../Service/AbstractService' +import { SharedVaultServiceEvent, SharedVaultServiceEventPayload } from './SharedVaultServiceEvent' +import { PendingSharedVaultInviteRecord } from './PendingSharedVaultInviteRecord' + +export interface SharedVaultServiceInterface + extends AbstractService { + createSharedVault(dto: { + name: string + description?: string + userInputtedPassword: string | undefined + storagePreference?: KeySystemRootKeyStorageMode + }): Promise + deleteSharedVault(sharedVault: SharedVaultListingInterface): Promise + + convertVaultToSharedVault(vault: VaultListingInterface): Promise + + inviteContactToSharedVault( + sharedVault: SharedVaultListingInterface, + contact: TrustedContactInterface, + permissions: SharedVaultPermission, + ): Promise + removeUserFromSharedVault( + sharedVault: SharedVaultListingInterface, + userUuid: string, + ): Promise + leaveSharedVault(sharedVault: SharedVaultListingInterface): Promise + getSharedVaultUsers(sharedVault: SharedVaultListingInterface): Promise + isSharedVaultUserSharedVaultOwner(user: SharedVaultUserServerHash): boolean + isCurrentUserSharedVaultAdmin(sharedVault: SharedVaultListingInterface): boolean + + getItemLastEditedBy(item: DecryptedItemInterface): TrustedContactInterface | undefined + getItemSharedBy(item: DecryptedItemInterface): TrustedContactInterface | undefined + + downloadInboundInvites(): Promise + getOutboundInvites( + sharedVault?: SharedVaultListingInterface, + ): Promise + acceptPendingSharedVaultInvite(pendingInvite: PendingSharedVaultInviteRecord): Promise + getCachedPendingInviteRecords(): PendingSharedVaultInviteRecord[] + getInvitableContactsForSharedVault(sharedVault: SharedVaultListingInterface): Promise + deleteInvite(invite: SharedVaultInviteServerHash): Promise +} diff --git a/packages/services/src/Domain/SharedVaults/UseCase/AcceptTrustedSharedVaultInvite.ts b/packages/services/src/Domain/SharedVaults/UseCase/AcceptTrustedSharedVaultInvite.ts new file mode 100644 index 000000000..55ef52c67 --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/UseCase/AcceptTrustedSharedVaultInvite.ts @@ -0,0 +1,29 @@ +import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface' +import { SyncServiceInterface } from './../../Sync/SyncServiceInterface' +import { AsymmetricMessageSharedVaultInvite } from '@standardnotes/models' +import { SharedVaultInvitesServerInterface } from '@standardnotes/api' +import { SharedVaultInviteServerHash } from '@standardnotes/responses' +import { HandleTrustedSharedVaultInviteMessage } from '../../AsymmetricMessage/UseCase/HandleTrustedSharedVaultInviteMessage' +import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface' + +export class AcceptTrustedSharedVaultInvite { + constructor( + private vaultInvitesServer: SharedVaultInvitesServerInterface, + private mutator: MutatorClientInterface, + private sync: SyncServiceInterface, + private contacts: ContactServiceInterface, + ) {} + + async execute(dto: { + invite: SharedVaultInviteServerHash + message: AsymmetricMessageSharedVaultInvite + }): Promise { + const useCase = new HandleTrustedSharedVaultInviteMessage(this.mutator, this.sync, this.contacts) + await useCase.execute(dto.message, dto.invite.shared_vault_uuid, dto.invite.sender_uuid) + + await this.vaultInvitesServer.acceptInvite({ + sharedVaultUuid: dto.invite.shared_vault_uuid, + inviteUuid: dto.invite.uuid, + }) + } +} diff --git a/packages/services/src/Domain/SharedVaults/UseCase/ConvertToSharedVault.ts b/packages/services/src/Domain/SharedVaults/UseCase/ConvertToSharedVault.ts new file mode 100644 index 000000000..e6cce9687 --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/UseCase/ConvertToSharedVault.ts @@ -0,0 +1,47 @@ +import { SyncServiceInterface } from './../../Sync/SyncServiceInterface' +import { SharedVaultListingInterface, VaultListingInterface, VaultListingMutator } from '@standardnotes/models' +import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses' +import { SharedVaultServerInterface } from '@standardnotes/api' +import { ItemManagerInterface } from '../../Item/ItemManagerInterface' +import { MoveItemsToVaultUseCase } from '../../Vaults/UseCase/MoveItemsToVault' +import { FilesClientInterface } from '@standardnotes/files' +import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface' + +export class ConvertToSharedVaultUseCase { + constructor( + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private sync: SyncServiceInterface, + private files: FilesClientInterface, + private sharedVaultServer: SharedVaultServerInterface, + ) {} + + async execute(dto: { vault: VaultListingInterface }): Promise { + if (dto.vault.isSharedVaultListing()) { + throw new Error('Cannot convert a shared vault to a shared vault') + } + + const serverResult = await this.sharedVaultServer.createSharedVault() + if (isErrorResponse(serverResult)) { + return ClientDisplayableError.FromString(`Failed to create shared vault ${serverResult}`) + } + + const serverVaultHash = serverResult.data.sharedVault + + const sharedVaultListing = await this.mutator.changeItem( + dto.vault, + (mutator) => { + mutator.sharing = { + sharedVaultUuid: serverVaultHash.uuid, + ownerUserUuid: serverVaultHash.user_uuid, + } + }, + ) + + const vaultItems = this.items.itemsBelongingToKeySystem(sharedVaultListing.systemIdentifier) + const moveToVaultUsecase = new MoveItemsToVaultUseCase(this.mutator, this.sync, this.files) + await moveToVaultUsecase.execute({ vault: sharedVaultListing, items: vaultItems }) + + return sharedVaultListing as SharedVaultListingInterface + } +} diff --git a/packages/services/src/Domain/SharedVaults/UseCase/CreateSharedVault.ts b/packages/services/src/Domain/SharedVaults/UseCase/CreateSharedVault.ts new file mode 100644 index 000000000..d13282db1 --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/UseCase/CreateSharedVault.ts @@ -0,0 +1,64 @@ +import { SyncServiceInterface } from './../../Sync/SyncServiceInterface' +import { + KeySystemRootKeyStorageMode, + SharedVaultListingInterface, + VaultListingInterface, + VaultListingMutator, +} from '@standardnotes/models' +import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses' +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { SharedVaultServerInterface } from '@standardnotes/api' +import { ItemManagerInterface } from '../../Item/ItemManagerInterface' +import { CreateVaultUseCase } from '../../Vaults/UseCase/CreateVault' +import { MoveItemsToVaultUseCase } from '../../Vaults/UseCase/MoveItemsToVault' +import { FilesClientInterface } from '@standardnotes/files' +import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface' + +export class CreateSharedVaultUseCase { + constructor( + private encryption: EncryptionProviderInterface, + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private sync: SyncServiceInterface, + private files: FilesClientInterface, + private sharedVaultServer: SharedVaultServerInterface, + ) {} + + async execute(dto: { + vaultName: string + vaultDescription?: string + userInputtedPassword: string | undefined + storagePreference: KeySystemRootKeyStorageMode + }): Promise { + const usecase = new CreateVaultUseCase(this.mutator, this.encryption, this.sync) + const privateVault = await usecase.execute({ + vaultName: dto.vaultName, + vaultDescription: dto.vaultDescription, + userInputtedPassword: dto.userInputtedPassword, + storagePreference: dto.storagePreference, + }) + + const serverResult = await this.sharedVaultServer.createSharedVault() + if (isErrorResponse(serverResult)) { + return ClientDisplayableError.FromString(`Failed to create shared vault ${JSON.stringify(serverResult)}`) + } + + const serverVaultHash = serverResult.data.sharedVault + + const sharedVaultListing = await this.mutator.changeItem( + privateVault, + (mutator) => { + mutator.sharing = { + sharedVaultUuid: serverVaultHash.uuid, + ownerUserUuid: serverVaultHash.user_uuid, + } + }, + ) + + const vaultItems = this.items.itemsBelongingToKeySystem(sharedVaultListing.systemIdentifier) + const moveToVaultUsecase = new MoveItemsToVaultUseCase(this.mutator, this.sync, this.files) + await moveToVaultUsecase.execute({ vault: sharedVaultListing, items: vaultItems }) + + return sharedVaultListing as SharedVaultListingInterface + } +} diff --git a/packages/services/src/Domain/SharedVaults/UseCase/DeleteExternalSharedVault.ts b/packages/services/src/Domain/SharedVaults/UseCase/DeleteExternalSharedVault.ts new file mode 100644 index 000000000..78819adb0 --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/UseCase/DeleteExternalSharedVault.ts @@ -0,0 +1,48 @@ +import { SyncServiceInterface } from './../../Sync/SyncServiceInterface' +import { StorageServiceInterface } from '../../Storage/StorageServiceInterface' +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { ItemManagerInterface } from '../../Item/ItemManagerInterface' +import { AnyItemInterface, VaultListingInterface } from '@standardnotes/models' +import { Uuids } from '@standardnotes/utils' +import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface' + +export class DeleteExternalSharedVaultUseCase { + constructor( + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private encryption: EncryptionProviderInterface, + private storage: StorageServiceInterface, + private sync: SyncServiceInterface, + ) {} + + async execute(vault: VaultListingInterface): Promise { + await this.deleteDataSharedByVaultUsers(vault) + await this.deleteDataOwnedByThisUser(vault) + await this.encryption.keys.deleteNonPersistentSystemRootKeysForVault(vault.systemIdentifier) + + void this.sync.sync({ sourceDescription: 'Not awaiting due to this event handler running from sync response' }) + } + + /** + * This data is shared with all vault users and does not belong to this particular user + * The data will be removed locally without syncing the items + */ + private async deleteDataSharedByVaultUsers(vault: VaultListingInterface): Promise { + const vaultItems = this.items + .allTrackedItems() + .filter((item) => item.key_system_identifier === vault.systemIdentifier) + this.items.removeItemsLocally(vaultItems as AnyItemInterface[]) + + const itemsKeys = this.encryption.keys.getKeySystemItemsKeys(vault.systemIdentifier) + this.items.removeItemsLocally(itemsKeys) + + await this.storage.deletePayloadsWithUuids([...Uuids(vaultItems), ...Uuids(itemsKeys)]) + } + + private async deleteDataOwnedByThisUser(vault: VaultListingInterface): Promise { + const rootKeys = this.encryption.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) + await this.mutator.setItemsToBeDeleted(rootKeys) + + await this.mutator.setItemToBeDeleted(vault) + } +} diff --git a/packages/services/src/Domain/SharedVaults/UseCase/DeleteSharedVault.ts b/packages/services/src/Domain/SharedVaults/UseCase/DeleteSharedVault.ts new file mode 100644 index 000000000..3950c75b2 --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/UseCase/DeleteSharedVault.ts @@ -0,0 +1,33 @@ +import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface' +import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses' +import { SharedVaultServerInterface } from '@standardnotes/api' +import { ItemManagerInterface } from '../../Item/ItemManagerInterface' +import { SharedVaultListingInterface } from '@standardnotes/models' +import { SyncServiceInterface } from '../../Sync/SyncServiceInterface' +import { DeleteVaultUseCase } from '../../Vaults/UseCase/DeleteVault' +import { EncryptionProviderInterface } from '@standardnotes/encryption' + +export class DeleteSharedVaultUseCase { + constructor( + private sharedVaultServer: SharedVaultServerInterface, + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private sync: SyncServiceInterface, + private encryption: EncryptionProviderInterface, + ) {} + + async execute(params: { sharedVault: SharedVaultListingInterface }): Promise { + const response = await this.sharedVaultServer.deleteSharedVault({ + sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid, + }) + + if (isErrorResponse(response)) { + return ClientDisplayableError.FromString(`Failed to delete vault ${response}`) + } + + const deleteUsecase = new DeleteVaultUseCase(this.items, this.mutator, this.encryption) + await deleteUsecase.execute(params.sharedVault) + + await this.sync.sync() + } +} diff --git a/packages/services/src/Domain/SharedVaults/UseCase/GetSharedVaultTrustedContacts.ts b/packages/services/src/Domain/SharedVaults/UseCase/GetSharedVaultTrustedContacts.ts new file mode 100644 index 000000000..580b71074 --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/UseCase/GetSharedVaultTrustedContacts.ts @@ -0,0 +1,23 @@ +import { SharedVaultUsersServerInterface } from '@standardnotes/api' +import { GetSharedVaultUsersUseCase } from './GetSharedVaultUsers' +import { SharedVaultListingInterface, TrustedContactInterface } from '@standardnotes/models' +import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface' +import { isNotUndefined } from '@standardnotes/utils' + +export class GetSharedVaultTrustedContacts { + constructor( + private contacts: ContactServiceInterface, + private sharedVaultUsersServer: SharedVaultUsersServerInterface, + ) {} + + async execute(vault: SharedVaultListingInterface): Promise { + const useCase = new GetSharedVaultUsersUseCase(this.sharedVaultUsersServer) + const users = await useCase.execute({ sharedVaultUuid: vault.sharing.sharedVaultUuid }) + if (!users) { + return undefined + } + + const contacts = users.map((user) => this.contacts.findTrustedContact(user.user_uuid)).filter(isNotUndefined) + return contacts + } +} diff --git a/packages/services/src/Domain/SharedVaults/UseCase/GetSharedVaultUsers.ts b/packages/services/src/Domain/SharedVaults/UseCase/GetSharedVaultUsers.ts new file mode 100644 index 000000000..eb022c6e4 --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/UseCase/GetSharedVaultUsers.ts @@ -0,0 +1,16 @@ +import { SharedVaultUserServerHash, isErrorResponse } from '@standardnotes/responses' +import { SharedVaultUsersServerInterface } from '@standardnotes/api' + +export class GetSharedVaultUsersUseCase { + constructor(private vaultUsersServer: SharedVaultUsersServerInterface) {} + + async execute(params: { sharedVaultUuid: string }): Promise { + const response = await this.vaultUsersServer.getSharedVaultUsers({ sharedVaultUuid: params.sharedVaultUuid }) + + if (isErrorResponse(response)) { + return undefined + } + + return response.data.users + } +} diff --git a/packages/services/src/Domain/SharedVaults/UseCase/InviteContactToSharedVault.ts b/packages/services/src/Domain/SharedVaults/UseCase/InviteContactToSharedVault.ts new file mode 100644 index 000000000..264b453e3 --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/UseCase/InviteContactToSharedVault.ts @@ -0,0 +1,63 @@ +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { ClientDisplayableError, SharedVaultInviteServerHash, SharedVaultPermission } from '@standardnotes/responses' +import { + TrustedContactInterface, + SharedVaultListingInterface, + AsymmetricMessagePayloadType, +} from '@standardnotes/models' +import { SharedVaultInvitesServerInterface } from '@standardnotes/api' +import { SendSharedVaultInviteUseCase } from './SendSharedVaultInviteUseCase' +import { PkcKeyPair } from '@standardnotes/sncrypto-common' + +export class InviteContactToSharedVaultUseCase { + constructor( + private encryption: EncryptionProviderInterface, + private sharedVaultInviteServer: SharedVaultInvitesServerInterface, + ) {} + + async execute(params: { + senderKeyPair: PkcKeyPair + senderSigningKeyPair: PkcKeyPair + sharedVault: SharedVaultListingInterface + sharedVaultContacts: TrustedContactInterface[] + recipient: TrustedContactInterface + permissions: SharedVaultPermission + }): Promise { + const keySystemRootKey = this.encryption.keys.getPrimaryKeySystemRootKey(params.sharedVault.systemIdentifier) + if (!keySystemRootKey) { + return ClientDisplayableError.FromString('Cannot add contact; key system root key not found') + } + + const delegatedContacts = params.sharedVaultContacts.filter( + (contact) => !contact.isMe && contact.contactUuid !== params.recipient.contactUuid, + ) + + const encryptedMessage = this.encryption.asymmetricallyEncryptMessage({ + message: { + type: AsymmetricMessagePayloadType.SharedVaultInvite, + data: { + recipientUuid: params.recipient.contactUuid, + rootKey: keySystemRootKey.content, + trustedContacts: delegatedContacts.map((contact) => contact.content), + metadata: { + name: params.sharedVault.name, + description: params.sharedVault.description, + }, + }, + }, + senderKeyPair: params.senderKeyPair, + senderSigningKeyPair: params.senderSigningKeyPair, + recipientPublicKey: params.recipient.publicKeySet.encryption, + }) + + const createInviteUseCase = new SendSharedVaultInviteUseCase(this.sharedVaultInviteServer) + const createInviteResult = await createInviteUseCase.execute({ + sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid, + recipientUuid: params.recipient.contactUuid, + encryptedMessage, + permissions: params.permissions, + }) + + return createInviteResult + } +} diff --git a/packages/services/src/Domain/SharedVaults/UseCase/LeaveSharedVault.ts b/packages/services/src/Domain/SharedVaults/UseCase/LeaveSharedVault.ts new file mode 100644 index 000000000..8f3cea34b --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/UseCase/LeaveSharedVault.ts @@ -0,0 +1,48 @@ +import { MutatorClientInterface } from './../../Mutator/MutatorClientInterface' +import { SyncServiceInterface } from './../../Sync/SyncServiceInterface' +import { StorageServiceInterface } from './../../Storage/StorageServiceInterface' +import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses' +import { SharedVaultUsersServerInterface } from '@standardnotes/api' +import { DeleteExternalSharedVaultUseCase } from './DeleteExternalSharedVault' +import { ItemManagerInterface } from '../../Item/ItemManagerInterface' +import { SharedVaultListingInterface } from '@standardnotes/models' +import { EncryptionProviderInterface } from '@standardnotes/encryption' + +export class LeaveVaultUseCase { + constructor( + private vaultUserServer: SharedVaultUsersServerInterface, + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private encryption: EncryptionProviderInterface, + private storage: StorageServiceInterface, + private sync: SyncServiceInterface, + ) {} + + async execute(params: { + sharedVault: SharedVaultListingInterface + userUuid: string + }): Promise { + const latestVaultListing = this.items.findItem(params.sharedVault.uuid) + if (!latestVaultListing) { + throw new Error(`LeaveVaultUseCase: Could not find vault ${params.sharedVault.uuid}`) + } + + const response = await this.vaultUserServer.deleteSharedVaultUser({ + sharedVaultUuid: latestVaultListing.sharing.sharedVaultUuid, + userUuid: params.userUuid, + }) + + if (isErrorResponse(response)) { + return ClientDisplayableError.FromString(`Failed to leave vault ${JSON.stringify(response)}`) + } + + const removeLocalItems = new DeleteExternalSharedVaultUseCase( + this.items, + this.mutator, + this.encryption, + this.storage, + this.sync, + ) + await removeLocalItems.execute(latestVaultListing) + } +} diff --git a/packages/services/src/Domain/SharedVaults/UseCase/NotifySharedVaultUsersOfRootKeyRotation.ts b/packages/services/src/Domain/SharedVaults/UseCase/NotifySharedVaultUsersOfRootKeyRotation.ts new file mode 100644 index 000000000..079ddf20a --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/UseCase/NotifySharedVaultUsersOfRootKeyRotation.ts @@ -0,0 +1,62 @@ +import { + AsymmetricMessageServerInterface, + SharedVaultInvitesServerInterface, + SharedVaultUsersServerInterface, +} from '@standardnotes/api' +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface' +import { SharedVaultListingInterface } from '@standardnotes/models' +import { ClientDisplayableError } from '@standardnotes/responses' +import { ReuploadSharedVaultInvitesAfterKeyRotationUseCase } from './ReuploadSharedVaultInvitesAfterKeyRotation' +import { SendSharedVaultRootKeyChangedMessageToAll } from './SendSharedVaultRootKeyChangedMessageToAll' + +export class NotifySharedVaultUsersOfRootKeyRotationUseCase { + constructor( + private sharedVaultUsersServer: SharedVaultUsersServerInterface, + private sharedVaultInvitesServer: SharedVaultInvitesServerInterface, + private messageServer: AsymmetricMessageServerInterface, + private encryption: EncryptionProviderInterface, + private contacts: ContactServiceInterface, + ) {} + + async execute(params: { + sharedVault: SharedVaultListingInterface + userUuid: string + }): Promise { + const errors: ClientDisplayableError[] = [] + const updatePendingInvitesUseCase = new ReuploadSharedVaultInvitesAfterKeyRotationUseCase( + this.encryption, + this.contacts, + this.sharedVaultInvitesServer, + this.sharedVaultUsersServer, + ) + + const updateExistingResults = await updatePendingInvitesUseCase.execute({ + sharedVault: params.sharedVault, + senderUuid: params.userUuid, + senderEncryptionKeyPair: this.encryption.getKeyPair(), + senderSigningKeyPair: this.encryption.getSigningKeyPair(), + }) + + errors.push(...updateExistingResults) + + const shareKeyUseCase = new SendSharedVaultRootKeyChangedMessageToAll( + this.encryption, + this.contacts, + this.sharedVaultUsersServer, + this.messageServer, + ) + + const shareKeyResults = await shareKeyUseCase.execute({ + keySystemIdentifier: params.sharedVault.systemIdentifier, + sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid, + senderUuid: params.userUuid, + senderEncryptionKeyPair: this.encryption.getKeyPair(), + senderSigningKeyPair: this.encryption.getSigningKeyPair(), + }) + + errors.push(...shareKeyResults) + + return errors + } +} diff --git a/packages/services/src/Domain/SharedVaults/UseCase/RemoveSharedVaultMember.ts b/packages/services/src/Domain/SharedVaults/UseCase/RemoveSharedVaultMember.ts new file mode 100644 index 000000000..abb03f348 --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/UseCase/RemoveSharedVaultMember.ts @@ -0,0 +1,17 @@ +import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses' +import { SharedVaultUsersServerInterface } from '@standardnotes/api' + +export class RemoveVaultMemberUseCase { + constructor(private vaultUserServer: SharedVaultUsersServerInterface) {} + + async execute(params: { sharedVaultUuid: string; userUuid: string }): Promise { + const response = await this.vaultUserServer.deleteSharedVaultUser({ + sharedVaultUuid: params.sharedVaultUuid, + userUuid: params.userUuid, + }) + + if (isErrorResponse(response)) { + return ClientDisplayableError.FromNetworkError(response) + } + } +} diff --git a/packages/services/src/Domain/SharedVaults/UseCase/ReuploadSharedVaultInvitesAfterKeyRotation.ts b/packages/services/src/Domain/SharedVaults/UseCase/ReuploadSharedVaultInvitesAfterKeyRotation.ts new file mode 100644 index 000000000..12847005e --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/UseCase/ReuploadSharedVaultInvitesAfterKeyRotation.ts @@ -0,0 +1,144 @@ +import { + KeySystemRootKeyContentSpecialized, + SharedVaultListingInterface, + TrustedContactInterface, +} from '@standardnotes/models' +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { + ClientDisplayableError, + SharedVaultInviteServerHash, + isClientDisplayableError, + isErrorResponse, +} from '@standardnotes/responses' +import { SharedVaultInvitesServerInterface, SharedVaultUsersServerInterface } from '@standardnotes/api' +import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface' +import { PkcKeyPair } from '@standardnotes/sncrypto-common' +import { InviteContactToSharedVaultUseCase } from './InviteContactToSharedVault' +import { GetSharedVaultTrustedContacts } from './GetSharedVaultTrustedContacts' + +type ReuploadAllSharedVaultInvitesDTO = { + sharedVault: SharedVaultListingInterface + senderUuid: string + senderEncryptionKeyPair: PkcKeyPair + senderSigningKeyPair: PkcKeyPair +} + +export class ReuploadSharedVaultInvitesAfterKeyRotationUseCase { + constructor( + private encryption: EncryptionProviderInterface, + private contacts: ContactServiceInterface, + private vaultInvitesServer: SharedVaultInvitesServerInterface, + private vaultUserServer: SharedVaultUsersServerInterface, + ) {} + + async execute(params: ReuploadAllSharedVaultInvitesDTO): Promise { + const keySystemRootKey = this.encryption.keys.getPrimaryKeySystemRootKey(params.sharedVault.systemIdentifier) + if (!keySystemRootKey) { + throw new Error(`Vault key not found for keySystemIdentifier ${params.sharedVault.systemIdentifier}`) + } + + const existingInvites = await this.getExistingInvites(params.sharedVault.sharing.sharedVaultUuid) + if (isClientDisplayableError(existingInvites)) { + return [existingInvites] + } + + const deleteResult = await this.deleteExistingInvites(params.sharedVault.sharing.sharedVaultUuid) + if (isClientDisplayableError(deleteResult)) { + return [deleteResult] + } + + const vaultContacts = await this.getVaultContacts(params.sharedVault) + if (vaultContacts.length === 0) { + return [] + } + + const errors: ClientDisplayableError[] = [] + + for (const invite of existingInvites) { + const contact = this.contacts.findTrustedContact(invite.user_uuid) + if (!contact) { + errors.push(ClientDisplayableError.FromString(`Contact not found for invite ${invite.user_uuid}`)) + continue + } + + const result = await this.sendNewInvite({ + usecaseDTO: params, + contact: contact, + previousInvite: invite, + keySystemRootKeyData: keySystemRootKey.content, + sharedVaultContacts: vaultContacts, + }) + + if (isClientDisplayableError(result)) { + errors.push(result) + } + } + + return errors + } + + private async getVaultContacts(sharedVault: SharedVaultListingInterface): Promise { + const usecase = new GetSharedVaultTrustedContacts(this.contacts, this.vaultUserServer) + const contacts = await usecase.execute(sharedVault) + if (!contacts) { + return [] + } + + return contacts + } + + private async getExistingInvites( + sharedVaultUuid: string, + ): Promise { + const response = await this.vaultInvitesServer.getOutboundUserInvites() + + if (isErrorResponse(response)) { + return ClientDisplayableError.FromString(`Failed to get outbound user invites ${response}`) + } + + const invites = response.data.invites + + return invites.filter((invite) => invite.shared_vault_uuid === sharedVaultUuid) + } + + private async deleteExistingInvites(sharedVaultUuid: string): Promise { + const response = await this.vaultInvitesServer.deleteAllSharedVaultInvites({ + sharedVaultUuid: sharedVaultUuid, + }) + + if (isErrorResponse(response)) { + return ClientDisplayableError.FromString(`Failed to delete existing invites ${response}`) + } + } + + private async sendNewInvite(params: { + usecaseDTO: ReuploadAllSharedVaultInvitesDTO + contact: TrustedContactInterface + previousInvite: SharedVaultInviteServerHash + keySystemRootKeyData: KeySystemRootKeyContentSpecialized + sharedVaultContacts: TrustedContactInterface[] + }): Promise { + const signatureResult = this.encryption.asymmetricSignatureVerifyDetached(params.previousInvite.encrypted_message) + if (!signatureResult.signatureVerified) { + return ClientDisplayableError.FromString('Failed to verify signature of previous invite') + } + + if (signatureResult.signaturePublicKey !== params.usecaseDTO.senderSigningKeyPair.publicKey) { + return ClientDisplayableError.FromString('Sender public key does not match signature') + } + + const usecase = new InviteContactToSharedVaultUseCase(this.encryption, this.vaultInvitesServer) + const result = await usecase.execute({ + senderKeyPair: params.usecaseDTO.senderEncryptionKeyPair, + senderSigningKeyPair: params.usecaseDTO.senderSigningKeyPair, + sharedVault: params.usecaseDTO.sharedVault, + sharedVaultContacts: params.sharedVaultContacts, + recipient: params.contact, + permissions: params.previousInvite.permissions, + }) + + if (isClientDisplayableError(result)) { + return result + } + } +} diff --git a/packages/services/src/Domain/SharedVaults/UseCase/SendSharedVaultInviteUseCase.ts b/packages/services/src/Domain/SharedVaults/UseCase/SendSharedVaultInviteUseCase.ts new file mode 100644 index 000000000..445d7f5e3 --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/UseCase/SendSharedVaultInviteUseCase.ts @@ -0,0 +1,31 @@ +import { + ClientDisplayableError, + SharedVaultInviteServerHash, + isErrorResponse, + SharedVaultPermission, +} from '@standardnotes/responses' +import { SharedVaultInvitesServerInterface } from '@standardnotes/api' + +export class SendSharedVaultInviteUseCase { + constructor(private vaultInvitesServer: SharedVaultInvitesServerInterface) {} + + async execute(params: { + sharedVaultUuid: string + recipientUuid: string + encryptedMessage: string + permissions: SharedVaultPermission + }): Promise { + const response = await this.vaultInvitesServer.createInvite({ + sharedVaultUuid: params.sharedVaultUuid, + recipientUuid: params.recipientUuid, + encryptedMessage: params.encryptedMessage, + permissions: params.permissions, + }) + + if (isErrorResponse(response)) { + return ClientDisplayableError.FromError(response.data.error) + } + + return response.data.invite + } +} diff --git a/packages/services/src/Domain/SharedVaults/UseCase/SendSharedVaultMetadataChangedMessageToAll.ts b/packages/services/src/Domain/SharedVaults/UseCase/SendSharedVaultMetadataChangedMessageToAll.ts new file mode 100644 index 000000000..2473cc4c9 --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/UseCase/SendSharedVaultMetadataChangedMessageToAll.ts @@ -0,0 +1,100 @@ +import { + AsymmetricMessagePayloadType, + AsymmetricMessageSharedVaultMetadataChanged, + SharedVaultListingInterface, + TrustedContactInterface, +} from '@standardnotes/models' +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { AsymmetricMessageServerHash, ClientDisplayableError, isClientDisplayableError } from '@standardnotes/responses' +import { AsymmetricMessageServerInterface, SharedVaultUsersServerInterface } from '@standardnotes/api' +import { GetSharedVaultUsersUseCase } from './GetSharedVaultUsers' +import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface' +import { PkcKeyPair } from '@standardnotes/sncrypto-common' +import { SendAsymmetricMessageUseCase } from '../../AsymmetricMessage/UseCase/SendAsymmetricMessageUseCase' + +export class SendSharedVaultMetadataChangedMessageToAll { + constructor( + private encryption: EncryptionProviderInterface, + private contacts: ContactServiceInterface, + private vaultUsersServer: SharedVaultUsersServerInterface, + private messageServer: AsymmetricMessageServerInterface, + ) {} + + async execute(params: { + vault: SharedVaultListingInterface + senderUuid: string + senderEncryptionKeyPair: PkcKeyPair + senderSigningKeyPair: PkcKeyPair + }): Promise { + const errors: ClientDisplayableError[] = [] + + const getUsersUseCase = new GetSharedVaultUsersUseCase(this.vaultUsersServer) + const users = await getUsersUseCase.execute({ sharedVaultUuid: params.vault.sharing.sharedVaultUuid }) + if (!users) { + return [ClientDisplayableError.FromString('Cannot send metadata changed message; users not found')] + } + + for (const user of users) { + if (user.user_uuid === params.senderUuid) { + continue + } + + const trustedContact = this.contacts.findTrustedContact(user.user_uuid) + if (!trustedContact) { + continue + } + + const sendMessageResult = await this.sendToContact({ + vault: params.vault, + senderKeyPair: params.senderEncryptionKeyPair, + senderSigningKeyPair: params.senderSigningKeyPair, + contact: trustedContact, + }) + + if (isClientDisplayableError(sendMessageResult)) { + errors.push(sendMessageResult) + } + } + + return errors + } + + private async sendToContact(params: { + vault: SharedVaultListingInterface + senderKeyPair: PkcKeyPair + senderSigningKeyPair: PkcKeyPair + contact: TrustedContactInterface + }): Promise { + const message: AsymmetricMessageSharedVaultMetadataChanged = { + type: AsymmetricMessagePayloadType.SharedVaultMetadataChanged, + data: { + recipientUuid: params.contact.contactUuid, + sharedVaultUuid: params.vault.sharing.sharedVaultUuid, + name: params.vault.name, + description: params.vault.description, + }, + } + + const encryptedMessage = this.encryption.asymmetricallyEncryptMessage({ + message: message, + senderKeyPair: params.senderKeyPair, + senderSigningKeyPair: params.senderSigningKeyPair, + recipientPublicKey: params.contact.publicKeySet.encryption, + }) + + const replaceabilityIdentifier = [ + AsymmetricMessagePayloadType.SharedVaultMetadataChanged, + params.vault.sharing.sharedVaultUuid, + params.vault.systemIdentifier, + ].join(':') + + const sendMessageUseCase = new SendAsymmetricMessageUseCase(this.messageServer) + const sendMessageResult = await sendMessageUseCase.execute({ + recipientUuid: params.contact.contactUuid, + encryptedMessage, + replaceabilityIdentifier, + }) + + return sendMessageResult + } +} diff --git a/packages/services/src/Domain/SharedVaults/UseCase/SendSharedVaultRootKeyChangedMessageToAll.ts b/packages/services/src/Domain/SharedVaults/UseCase/SendSharedVaultRootKeyChangedMessageToAll.ts new file mode 100644 index 000000000..8656afc89 --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/UseCase/SendSharedVaultRootKeyChangedMessageToAll.ts @@ -0,0 +1,103 @@ +import { + AsymmetricMessagePayloadType, + AsymmetricMessageSharedVaultRootKeyChanged, + KeySystemIdentifier, + TrustedContactInterface, +} from '@standardnotes/models' +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { AsymmetricMessageServerHash, ClientDisplayableError, isClientDisplayableError } from '@standardnotes/responses' +import { AsymmetricMessageServerInterface, SharedVaultUsersServerInterface } from '@standardnotes/api' +import { GetSharedVaultUsersUseCase } from './GetSharedVaultUsers' +import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface' +import { PkcKeyPair } from '@standardnotes/sncrypto-common' +import { SendAsymmetricMessageUseCase } from '../../AsymmetricMessage/UseCase/SendAsymmetricMessageUseCase' + +export class SendSharedVaultRootKeyChangedMessageToAll { + constructor( + private encryption: EncryptionProviderInterface, + private contacts: ContactServiceInterface, + private vaultUsersServer: SharedVaultUsersServerInterface, + private messageServer: AsymmetricMessageServerInterface, + ) {} + + async execute(params: { + keySystemIdentifier: KeySystemIdentifier + sharedVaultUuid: string + senderUuid: string + senderEncryptionKeyPair: PkcKeyPair + senderSigningKeyPair: PkcKeyPair + }): Promise { + const errors: ClientDisplayableError[] = [] + + const getUsersUseCase = new GetSharedVaultUsersUseCase(this.vaultUsersServer) + const users = await getUsersUseCase.execute({ sharedVaultUuid: params.sharedVaultUuid }) + if (!users) { + return [ClientDisplayableError.FromString('Cannot send root key changed message; users not found')] + } + + for (const user of users) { + if (user.user_uuid === params.senderUuid) { + continue + } + + const trustedContact = this.contacts.findTrustedContact(user.user_uuid) + if (!trustedContact) { + continue + } + + const sendMessageResult = await this.sendToContact({ + keySystemIdentifier: params.keySystemIdentifier, + sharedVaultUuid: params.sharedVaultUuid, + senderKeyPair: params.senderEncryptionKeyPair, + senderSigningKeyPair: params.senderSigningKeyPair, + contact: trustedContact, + }) + + if (isClientDisplayableError(sendMessageResult)) { + errors.push(sendMessageResult) + } + } + + return errors + } + + private async sendToContact(params: { + keySystemIdentifier: KeySystemIdentifier + sharedVaultUuid: string + senderKeyPair: PkcKeyPair + senderSigningKeyPair: PkcKeyPair + contact: TrustedContactInterface + }): Promise { + const keySystemRootKey = this.encryption.keys.getPrimaryKeySystemRootKey(params.keySystemIdentifier) + if (!keySystemRootKey) { + throw new Error(`Vault key not found for keySystemIdentifier ${params.keySystemIdentifier}`) + } + + const message: AsymmetricMessageSharedVaultRootKeyChanged = { + type: AsymmetricMessagePayloadType.SharedVaultRootKeyChanged, + data: { recipientUuid: params.contact.contactUuid, rootKey: keySystemRootKey.content }, + } + + const encryptedMessage = this.encryption.asymmetricallyEncryptMessage({ + message: message, + senderKeyPair: params.senderKeyPair, + senderSigningKeyPair: params.senderSigningKeyPair, + recipientPublicKey: params.contact.publicKeySet.encryption, + }) + + const replaceabilityIdentifier = [ + AsymmetricMessagePayloadType.SharedVaultRootKeyChanged, + params.sharedVaultUuid, + params.keySystemIdentifier, + ].join(':') + + const sendMessageUseCase = new SendAsymmetricMessageUseCase(this.messageServer) + const sendMessageResult = await sendMessageUseCase.execute({ + recipientUuid: params.contact.contactUuid, + encryptedMessage, + replaceabilityIdentifier, + }) + + return sendMessageResult + } +} diff --git a/packages/services/src/Domain/SharedVaults/UseCase/ShareContactWithAllMembersOfSharedVault.ts b/packages/services/src/Domain/SharedVaults/UseCase/ShareContactWithAllMembersOfSharedVault.ts new file mode 100644 index 000000000..0e6b63a3b --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/UseCase/ShareContactWithAllMembersOfSharedVault.ts @@ -0,0 +1,78 @@ +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses' +import { + TrustedContactInterface, + SharedVaultListingInterface, + AsymmetricMessagePayloadType, +} from '@standardnotes/models' +import { AsymmetricMessageServerInterface, SharedVaultUsersServerInterface } from '@standardnotes/api' +import { PkcKeyPair } from '@standardnotes/sncrypto-common' +import { ContactServiceInterface } from '../../Contacts/ContactServiceInterface' +import { SendAsymmetricMessageUseCase } from '../../AsymmetricMessage/UseCase/SendAsymmetricMessageUseCase' + +export class ShareContactWithAllMembersOfSharedVaultUseCase { + constructor( + private contacts: ContactServiceInterface, + private encryption: EncryptionProviderInterface, + private sharedVaultUsersServer: SharedVaultUsersServerInterface, + private messageServer: AsymmetricMessageServerInterface, + ) {} + + async execute(params: { + senderKeyPair: PkcKeyPair + senderSigningKeyPair: PkcKeyPair + senderUserUuid: string + sharedVault: SharedVaultListingInterface + contactToShare: TrustedContactInterface + }): Promise { + if (params.sharedVault.sharing.ownerUserUuid !== params.senderUserUuid) { + return ClientDisplayableError.FromString('Cannot share contact; user is not the owner of the shared vault') + } + + const usersResponse = await this.sharedVaultUsersServer.getSharedVaultUsers({ + sharedVaultUuid: params.sharedVault.sharing.sharedVaultUuid, + }) + + if (isErrorResponse(usersResponse)) { + return ClientDisplayableError.FromString('Cannot share contact; shared vault users not found') + } + + const users = usersResponse.data.users + if (users.length === 0) { + return + } + + const messageSendUseCase = new SendAsymmetricMessageUseCase(this.messageServer) + + for (const vaultUser of users) { + if (vaultUser.user_uuid === params.senderUserUuid) { + continue + } + + if (vaultUser.user_uuid === params.contactToShare.contactUuid) { + continue + } + + const vaultUserAsContact = this.contacts.findTrustedContact(vaultUser.user_uuid) + if (!vaultUserAsContact) { + continue + } + + const encryptedMessage = this.encryption.asymmetricallyEncryptMessage({ + message: { + type: AsymmetricMessagePayloadType.ContactShare, + data: { recipientUuid: vaultUserAsContact.contactUuid, trustedContact: params.contactToShare.content }, + }, + senderKeyPair: params.senderKeyPair, + senderSigningKeyPair: params.senderSigningKeyPair, + recipientPublicKey: vaultUserAsContact.publicKeySet.encryption, + }) + + await messageSendUseCase.execute({ + recipientUuid: vaultUserAsContact.contactUuid, + encryptedMessage, + replaceabilityIdentifier: undefined, + }) + } + } +} diff --git a/packages/services/src/Domain/SharedVaults/UseCase/UpdateSharedVaultInvite.ts b/packages/services/src/Domain/SharedVaults/UseCase/UpdateSharedVaultInvite.ts new file mode 100644 index 000000000..847888d87 --- /dev/null +++ b/packages/services/src/Domain/SharedVaults/UseCase/UpdateSharedVaultInvite.ts @@ -0,0 +1,31 @@ +import { + ClientDisplayableError, + SharedVaultInviteServerHash, + isErrorResponse, + SharedVaultPermission, +} from '@standardnotes/responses' +import { SharedVaultInvitesServerInterface } from '@standardnotes/api' + +export class UpdateSharedVaultInviteUseCase { + constructor(private vaultInvitesServer: SharedVaultInvitesServerInterface) {} + + async execute(params: { + sharedVaultUuid: string + inviteUuid: string + encryptedMessage: string + permissions: SharedVaultPermission + }): Promise { + const response = await this.vaultInvitesServer.updateInvite({ + sharedVaultUuid: params.sharedVaultUuid, + inviteUuid: params.inviteUuid, + encryptedMessage: params.encryptedMessage, + permissions: params.permissions, + }) + + if (isErrorResponse(response)) { + return ClientDisplayableError.FromError(response.data.error) + } + + return response.data.invite + } +} diff --git a/packages/services/src/Domain/Singleton/SingletonManagerInterface.ts b/packages/services/src/Domain/Singleton/SingletonManagerInterface.ts new file mode 100644 index 000000000..c3dafd0be --- /dev/null +++ b/packages/services/src/Domain/Singleton/SingletonManagerInterface.ts @@ -0,0 +1,26 @@ +import { DecryptedItemInterface, ItemContent, Predicate, PredicateInterface } from '@standardnotes/models' +import { ContentType } from '@standardnotes/common' + +export interface SingletonManagerInterface { + findSingleton( + contentType: ContentType, + predicate: PredicateInterface, + ): T | undefined + + findOrCreateContentTypeSingleton< + C extends ItemContent = ItemContent, + T extends DecryptedItemInterface = DecryptedItemInterface, + >( + contentType: ContentType, + createContent: ItemContent, + ): Promise + + findOrCreateSingleton< + C extends ItemContent = ItemContent, + T extends DecryptedItemInterface = DecryptedItemInterface, + >( + predicate: Predicate, + contentType: ContentType, + createContent: ItemContent, + ): Promise +} diff --git a/packages/services/src/Domain/Storage/StorageKeys.ts b/packages/services/src/Domain/Storage/StorageKeys.ts index 2af95426c..a064d1f03 100644 --- a/packages/services/src/Domain/Storage/StorageKeys.ts +++ b/packages/services/src/Domain/Storage/StorageKeys.ts @@ -47,6 +47,7 @@ export enum StorageKey { PlaintextBackupsLocation = 'plaintext_backups_location', FileBackupsEnabled = 'file_backups_enabled', FileBackupsLocation = 'file_backups_location', + VaultSelectionOptions = 'vault_selection_options', } export enum NonwrappedStorageKey { diff --git a/packages/services/src/Domain/Storage/StorageServiceInterface.ts b/packages/services/src/Domain/Storage/StorageServiceInterface.ts index 1663f62f0..e89e01068 100644 --- a/packages/services/src/Domain/Storage/StorageServiceInterface.ts +++ b/packages/services/src/Domain/Storage/StorageServiceInterface.ts @@ -8,14 +8,16 @@ import { StoragePersistencePolicies, StorageValueModes } from './StorageTypes' export interface StorageServiceInterface { getAllRawPayloads(): Promise + getAllKeys(mode?: StorageValueModes): string[] getValue(key: string, mode?: StorageValueModes, defaultValue?: T): T canDecryptWithKey(key: RootKeyInterface): Promise savePayload(payload: PayloadInterface): Promise savePayloads(decryptedPayloads: PayloadInterface[]): Promise - setValue(key: string, value: unknown, mode?: StorageValueModes): void + setValue(key: string, value: T, mode?: StorageValueModes): void removeValue(key: string, mode?: StorageValueModes): Promise setPersistencePolicy(persistencePolicy: StoragePersistencePolicies): Promise clearAllData(): Promise - forceDeletePayloads(payloads: FullyFormedPayloadInterface[]): Promise + deletePayloads(payloads: FullyFormedPayloadInterface[]): Promise + deletePayloadsWithUuids(uuids: string[]): Promise clearAllPayloads(): Promise } diff --git a/packages/services/src/Domain/Strings/InfoStrings.ts b/packages/services/src/Domain/Strings/InfoStrings.ts index cd220ca05..d474b4138 100644 --- a/packages/services/src/Domain/Strings/InfoStrings.ts +++ b/packages/services/src/Domain/Strings/InfoStrings.ts @@ -1,9 +1,6 @@ export const InfoStrings = { AccountDeleted: 'Your account has been successfully deleted.', - UnsupportedBackupFileVersion: - 'This backup file was created using a newer version of the application and cannot be imported here. Please update your application and try again.', - BackupFileMoreRecentThanAccount: - "This backup file was created using a newer encryption version than your account's. Please run the available encryption upgrade and try again.", + InvalidNote: "The note you are attempting to save can not be found or has been deleted. Changes you make will not be synced. Please copy this note's text and start a new note.", } diff --git a/packages/services/src/Domain/Strings/Messages.ts b/packages/services/src/Domain/Strings/Messages.ts index ec794ca5e..0bf160cbb 100644 --- a/packages/services/src/Domain/Strings/Messages.ts +++ b/packages/services/src/Domain/Strings/Messages.ts @@ -167,6 +167,8 @@ export const ChallengeStrings = { DisableMfa: 'Authentication is required to disable two-factor authentication', DeleteAccount: 'Authentication is required to delete your account', ListedAuthorization: 'Authentication is required to approve this note for Listed', + UnlockVault: (vaultName: string) => `Unlock ${vaultName}`, + EnterVaultPassword: 'Enter the password for this vault', } export const ErrorAlertStrings = { diff --git a/packages/services/src/Domain/Sync/SyncOptions.ts b/packages/services/src/Domain/Sync/SyncOptions.ts index 96e0352e3..879ba5a60 100644 --- a/packages/services/src/Domain/Sync/SyncOptions.ts +++ b/packages/services/src/Domain/Sync/SyncOptions.ts @@ -19,4 +19,10 @@ export type SyncOptions = { * and before the sync request is network dispatched */ onPresyncSave?: () => void + + /** If supplied, the sync will be exclusive to items in these sharedVaults */ + sharedVaultUuids?: string[] + + /** If true and sharedVaultUuids is present, excludes sending global syncToken as part of request */ + syncSharedVaultsFromScratch?: boolean } diff --git a/packages/services/src/Domain/Sync/SyncServiceInterface.ts b/packages/services/src/Domain/Sync/SyncServiceInterface.ts index 7d5047db5..9a8005eba 100644 --- a/packages/services/src/Domain/Sync/SyncServiceInterface.ts +++ b/packages/services/src/Domain/Sync/SyncServiceInterface.ts @@ -2,8 +2,10 @@ import { FullyFormedPayloadInterface } from '@standardnotes/models' import { SyncOptions } from './SyncOptions' +import { AbstractService } from '../Service/AbstractService' +import { SyncEvent } from '../Event/SyncEvent' -export interface SyncServiceInterface { +export interface SyncServiceInterface extends AbstractService { sync(options?: Partial): Promise resetSyncState(): void markAllItemsAsNeedingSyncAndPersist(): Promise @@ -11,4 +13,5 @@ export interface SyncServiceInterface { persistPayloads(payloads: FullyFormedPayloadInterface[]): Promise lockSyncing(): void unlockSyncing(): void + syncSharedVaultsFromScratch(sharedVaultUuids: string[]): Promise } diff --git a/packages/services/src/Domain/User/UserClientInterface.ts b/packages/services/src/Domain/User/UserClientInterface.ts index 6c4404986..62b20df58 100644 --- a/packages/services/src/Domain/User/UserClientInterface.ts +++ b/packages/services/src/Domain/User/UserClientInterface.ts @@ -1,8 +1,48 @@ import { Base64String } from '@standardnotes/sncrypto-common' -import { UserRequestType } from '@standardnotes/common' +import { Either, UserRequestType } from '@standardnotes/common' import { DeinitSource } from '../Application/DeinitSource' +import { UserRegistrationResponseBody } from '@standardnotes/api' +import { HttpError, HttpResponse, SignInResponse } from '@standardnotes/responses' +import { AbstractService } from '../Service/AbstractService' -export interface UserClientInterface { +export type CredentialsChangeFunctionResponse = { error?: HttpError } + +export enum AccountEvent { + SignedInOrRegistered = 'SignedInOrRegistered', + SignedOut = 'SignedOut', +} + +export interface SignedInOrRegisteredEventPayload { + ephemeral: boolean + mergeLocal: boolean + awaitSync: boolean + checkIntegrity: boolean +} + +export interface SignedOutEventPayload { + source: DeinitSource +} + +export interface AccountEventData { + payload: Either +} + +export interface UserClientInterface extends AbstractService { + isSignedIn(): boolean + register( + email: string, + password: string, + ephemeral: boolean, + mergeLocal: boolean, + ): Promise + signIn( + email: string, + password: string, + strict: boolean, + ephemeral: boolean, + mergeLocal: boolean, + awaitSync: boolean, + ): Promise> deleteAccount(): Promise<{ error: boolean message?: string @@ -10,4 +50,9 @@ export interface UserClientInterface { signOut(force?: boolean, source?: DeinitSource): Promise submitUserRequest(requestType: UserRequestType): Promise populateSessionFromDemoShareToken(token: Base64String): Promise + updateAccountWithFirstTimeKeyPair(): Promise<{ + success?: true + canceled?: true + error?: { message: string } + }> } diff --git a/packages/services/src/Domain/User/UserService.ts b/packages/services/src/Domain/User/UserService.ts index 8f4ee14b8..c700b66eb 100644 --- a/packages/services/src/Domain/User/UserService.ts +++ b/packages/services/src/Domain/User/UserService.ts @@ -1,9 +1,15 @@ import { Base64String } from '@standardnotes/sncrypto-common' import { EncryptionProviderInterface, SNRootKey, SNRootKeyParams } from '@standardnotes/encryption' -import { HttpResponse, SignInResponse, User, HttpError, isErrorResponse } from '@standardnotes/responses' -import { Either, KeyParamsOrigination, UserRequestType } from '@standardnotes/common' +import { HttpResponse, SignInResponse, User, isErrorResponse } from '@standardnotes/responses' +import { KeyParamsOrigination, UserRequestType } from '@standardnotes/common' import { UuidGenerator } from '@standardnotes/utils' import { UserApiServiceInterface, UserRegistrationResponseBody } from '@standardnotes/api' +import { + AccountEventData, + AccountEvent, + SignedInOrRegisteredEventPayload, + CredentialsChangeFunctionResponse, +} from '@standardnotes/services' import * as Messages from '../Strings/Messages' import { InfoStrings } from '../Strings/InfoStrings' @@ -28,28 +34,6 @@ import { ProtectionsClientInterface } from '../Protection/ProtectionClientInterf import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface' import { InternalEventInterface } from '../Internal/InternalEventInterface' -export type CredentialsChangeFunctionResponse = { error?: HttpError } - -export enum AccountEvent { - SignedInOrRegistered = 'SignedInOrRegistered', - SignedOut = 'SignedOut', -} - -export interface SignedInOrRegisteredEventPayload { - ephemeral: boolean - mergeLocal: boolean - awaitSync: boolean - checkIntegrity: boolean -} - -export interface SignedOutEventPayload { - source: DeinitSource -} - -export interface AccountEventData { - payload: Either -} - export class UserService extends AbstractService implements UserClientInterface, InternalEventHandlerInterface @@ -125,6 +109,10 @@ export class UserService ;(this.userApiService as unknown) = undefined } + isSignedIn(): boolean { + return this.sessionManager.isSignedIn() + } + /** * @param mergeLocal Whether to merge existing offline data into account. If false, * any pre-existing data will be fully deleted upon success. @@ -352,6 +340,20 @@ export class UserService } } + async updateAccountWithFirstTimeKeyPair(): Promise<{ + success?: true + canceled?: true + error?: { message: string } + }> { + if (!this.sessionManager.isUserMissingKeyPair()) { + throw Error('Cannot update account with first time keypair if user already has a keypair') + } + + const result = await this.performProtocolUpgrade() + + return result + } + public async performProtocolUpgrade(): Promise<{ success?: true canceled?: true @@ -524,7 +526,7 @@ export class UserService private async rewriteItemsKeys(): Promise { const itemsKeys = this.itemManager.getDisplayableItemsKeys() const payloads = itemsKeys.map((key) => key.payloadRepresentation()) - await this.storageService.forceDeletePayloads(payloads) + await this.storageService.deletePayloads(payloads) await this.syncService.persistPayloads(payloads) } @@ -571,7 +573,7 @@ export class UserService const user = this.sessionManager.getUser() as User const currentEmail = user.email - const rootKeys = await this.recomputeRootKeysForCredentialChange({ + const { currentRootKey, newRootKey } = await this.recomputeRootKeysForCredentialChange({ currentPassword: parameters.currentPassword, currentEmail, origination: parameters.origination, @@ -583,8 +585,8 @@ export class UserService /** Now, change the credentials on the server. Roll back on failure */ const { response } = await this.sessionManager.changeCredentials({ - currentServerPassword: rootKeys.currentRootKey.serverPassword as string, - newRootKey: rootKeys.newRootKey, + currentServerPassword: currentRootKey.serverPassword as string, + newRootKey: newRootKey, wrappingKey, newEmail: parameters.newEmail, }) @@ -596,7 +598,7 @@ export class UserService } const rollback = await this.protocolService.createNewItemsKeyWithRollback() - await this.protocolService.reencryptItemsKeys() + await this.protocolService.reencryptApplicableItemsAfterUserRootKeyChange() await this.syncService.sync({ awaitAll: true }) const defaultItemsKey = this.protocolService.getSureDefaultItemsKey() @@ -604,11 +606,11 @@ export class UserService if (!itemsKeyWasSynced) { await this.sessionManager.changeCredentials({ - currentServerPassword: rootKeys.newRootKey.serverPassword as string, - newRootKey: rootKeys.currentRootKey, + currentServerPassword: newRootKey.serverPassword as string, + newRootKey: currentRootKey, wrappingKey, }) - await this.protocolService.reencryptItemsKeys() + await this.protocolService.reencryptApplicableItemsAfterUserRootKeyChange() await rollback() await this.syncService.sync({ awaitAll: true }) diff --git a/packages/services/src/Domain/UserEvent/UserEventService.ts b/packages/services/src/Domain/UserEvent/UserEventService.ts new file mode 100644 index 000000000..c9cf580af --- /dev/null +++ b/packages/services/src/Domain/UserEvent/UserEventService.ts @@ -0,0 +1,38 @@ +import { UserEventServerHash } from '@standardnotes/responses' +import { SyncEvent, SyncEventReceivedUserEventsData } from '../Event/SyncEvent' +import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' +import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface' +import { InternalEventInterface } from '../Internal/InternalEventInterface' +import { AbstractService } from '../Service/AbstractService' +import { UserEventServiceEventPayload, UserEventServiceEvent } from './UserEventServiceEvent' + +export class UserEventService + extends AbstractService + implements InternalEventHandlerInterface +{ + constructor(internalEventBus: InternalEventBusInterface) { + super(internalEventBus) + + internalEventBus.addEventHandler(this, SyncEvent.ReceivedUserEvents) + } + + async handleEvent(event: InternalEventInterface): Promise { + if (event.type === SyncEvent.ReceivedUserEvents) { + return this.handleReceivedUserEvents(event.payload as SyncEventReceivedUserEventsData) + } + } + + private async handleReceivedUserEvents(userEvents: UserEventServerHash[]): Promise { + if (userEvents.length === 0) { + return + } + + for (const serverEvent of userEvents) { + const serviceEvent: UserEventServiceEventPayload = { + eventPayload: JSON.parse(serverEvent.event_payload), + } + + await this.notifyEventSync(UserEventServiceEvent.UserEventReceived, serviceEvent) + } + } +} diff --git a/packages/services/src/Domain/UserEvent/UserEventServiceEvent.ts b/packages/services/src/Domain/UserEvent/UserEventServiceEvent.ts new file mode 100644 index 000000000..0eb8b4afc --- /dev/null +++ b/packages/services/src/Domain/UserEvent/UserEventServiceEvent.ts @@ -0,0 +1,9 @@ +import { UserEventPayload } from '@standardnotes/responses' + +export enum UserEventServiceEvent { + UserEventReceived = 'UserEventReceived', +} + +export type UserEventServiceEventPayload = { + eventPayload: UserEventPayload +} diff --git a/packages/services/src/Domain/Vaults/ChangeVaultOptionsDTO.ts b/packages/services/src/Domain/Vaults/ChangeVaultOptionsDTO.ts new file mode 100644 index 000000000..2001dd8c3 --- /dev/null +++ b/packages/services/src/Domain/Vaults/ChangeVaultOptionsDTO.ts @@ -0,0 +1,10 @@ +import { KeySystemRootKeyPasswordType, KeySystemRootKeyStorageMode, VaultListingInterface } from '@standardnotes/models' + +export type ChangeVaultOptionsDTO = { + vault: VaultListingInterface + newPasswordType: + | { passwordType: KeySystemRootKeyPasswordType.Randomized } + | { passwordType: KeySystemRootKeyPasswordType.UserInputted; userInputtedPassword: string } + | undefined + newKeyStorageMode: KeySystemRootKeyStorageMode | undefined +} diff --git a/packages/services/src/Domain/Vaults/UseCase/ChangeVaultKeyOptions.ts b/packages/services/src/Domain/Vaults/UseCase/ChangeVaultKeyOptions.ts new file mode 100644 index 000000000..98600d7e8 --- /dev/null +++ b/packages/services/src/Domain/Vaults/UseCase/ChangeVaultKeyOptions.ts @@ -0,0 +1,150 @@ +import { MutatorClientInterface, SyncServiceInterface } from '@standardnotes/services' +import { ItemManagerInterface } from '../../Item/ItemManagerInterface' +import { + KeySystemRootKeyPasswordType, + KeySystemRootKeyStorageMode, + VaultListingInterface, + VaultListingMutator, +} from '@standardnotes/models' +import { EncryptionProviderInterface, KeySystemKeyManagerInterface } from '@standardnotes/encryption' +import { ChangeVaultOptionsDTO } from '../ChangeVaultOptionsDTO' +import { GetVaultUseCase } from './GetVault' +import { assert } from '@standardnotes/utils' + +export class ChangeVaultKeyOptionsUseCase { + constructor( + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private sync: SyncServiceInterface, + private encryption: EncryptionProviderInterface, + ) {} + + private get keys(): KeySystemKeyManagerInterface { + return this.encryption.keys + } + + async execute(dto: ChangeVaultOptionsDTO): Promise { + const useStorageMode = dto.newKeyStorageMode ?? dto.vault.keyStorageMode + + if (dto.newPasswordType) { + if (dto.vault.keyPasswordType === dto.newPasswordType.passwordType) { + throw new Error('Vault password type is already set to this type') + } + + if (dto.newPasswordType.passwordType === KeySystemRootKeyPasswordType.UserInputted) { + if (!dto.newPasswordType.userInputtedPassword) { + throw new Error('User inputted password is required') + } + await this.changePasswordTypeToUserInputted(dto.vault, dto.newPasswordType.userInputtedPassword, useStorageMode) + } else if (dto.newPasswordType.passwordType === KeySystemRootKeyPasswordType.Randomized) { + await this.changePasswordTypeToRandomized(dto.vault, useStorageMode) + } + } + + if (dto.newKeyStorageMode) { + const usecase = new GetVaultUseCase(this.items) + const latestVault = usecase.execute({ keySystemIdentifier: dto.vault.systemIdentifier }) + assert(latestVault) + + if (latestVault.rootKeyParams.passwordType !== KeySystemRootKeyPasswordType.UserInputted) { + throw new Error('Vault uses randomized password and cannot change its storage preference') + } + + if (dto.newKeyStorageMode === latestVault.keyStorageMode) { + throw new Error('Vault already uses this storage preference') + } + + if ( + dto.newKeyStorageMode === KeySystemRootKeyStorageMode.Local || + dto.newKeyStorageMode === KeySystemRootKeyStorageMode.Ephemeral + ) { + await this.changeStorageModeToLocalOrEphemeral(latestVault, dto.newKeyStorageMode) + } else if (dto.newKeyStorageMode === KeySystemRootKeyStorageMode.Synced) { + await this.changeStorageModeToSynced(latestVault) + } + } + + await this.sync.sync() + } + + private async changePasswordTypeToUserInputted( + vault: VaultListingInterface, + userInputtedPassword: string, + storageMode: KeySystemRootKeyStorageMode, + ): Promise { + const newRootKey = this.encryption.createUserInputtedKeySystemRootKey({ + systemIdentifier: vault.systemIdentifier, + userInputtedPassword: userInputtedPassword, + }) + + if (storageMode === KeySystemRootKeyStorageMode.Synced) { + await this.mutator.insertItem(newRootKey, true) + } else { + this.encryption.keys.intakeNonPersistentKeySystemRootKey(newRootKey, storageMode) + } + + await this.mutator.changeItem(vault, (mutator) => { + mutator.rootKeyParams = newRootKey.keyParams + }) + + await this.encryption.reencryptKeySystemItemsKeysForVault(vault.systemIdentifier) + } + + private async changePasswordTypeToRandomized( + vault: VaultListingInterface, + storageMode: KeySystemRootKeyStorageMode, + ): Promise { + const newRootKey = this.encryption.createRandomizedKeySystemRootKey({ + systemIdentifier: vault.systemIdentifier, + }) + + if (storageMode !== KeySystemRootKeyStorageMode.Synced) { + throw new Error('Cannot change to randomized password if root key storage is not synced') + } + + await this.mutator.changeItem(vault, (mutator) => { + mutator.rootKeyParams = newRootKey.keyParams + }) + + await this.mutator.insertItem(newRootKey, true) + + await this.encryption.reencryptKeySystemItemsKeysForVault(vault.systemIdentifier) + } + + private async changeStorageModeToLocalOrEphemeral( + vault: VaultListingInterface, + newKeyStorageMode: KeySystemRootKeyStorageMode, + ): Promise { + const primaryKey = this.keys.getPrimaryKeySystemRootKey(vault.systemIdentifier) + if (!primaryKey) { + throw new Error('No primary key found') + } + + this.keys.intakeNonPersistentKeySystemRootKey(primaryKey, newKeyStorageMode) + await this.keys.deleteAllSyncedKeySystemRootKeys(vault.systemIdentifier) + + await this.mutator.changeItem(vault, (mutator) => { + mutator.keyStorageMode = newKeyStorageMode + }) + + await this.sync.sync() + } + + private async changeStorageModeToSynced(vault: VaultListingInterface): Promise { + const allRootKeys = this.keys.getAllKeySystemRootKeysForVault(vault.systemIdentifier) + const syncedRootKeys = this.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) + + for (const key of allRootKeys) { + const existingSyncedKey = syncedRootKeys.find((syncedKey) => syncedKey.token === key.token) + if (existingSyncedKey) { + continue + } + + await this.mutator.insertItem(key) + } + + await this.mutator.changeItem(vault, (mutator) => { + mutator.keyStorageMode = KeySystemRootKeyStorageMode.Synced + }) + } +} diff --git a/packages/services/src/Domain/Vaults/UseCase/CreateVault.ts b/packages/services/src/Domain/Vaults/UseCase/CreateVault.ts new file mode 100644 index 000000000..e51cc3625 --- /dev/null +++ b/packages/services/src/Domain/Vaults/UseCase/CreateVault.ts @@ -0,0 +1,115 @@ +import { SyncServiceInterface } from './../../Sync/SyncServiceInterface' +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { UuidGenerator } from '@standardnotes/utils' +import { + KeySystemRootKeyParamsInterface, + KeySystemRootKeyPasswordType, + VaultListingContentSpecialized, + VaultListingInterface, + KeySystemRootKeyStorageMode, + FillItemContentSpecialized, + KeySystemRootKeyInterface, +} from '@standardnotes/models' +import { ContentType } from '@standardnotes/common' +import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface' + +export class CreateVaultUseCase { + constructor( + private mutator: MutatorClientInterface, + private encryption: EncryptionProviderInterface, + private sync: SyncServiceInterface, + ) {} + + async execute(dto: { + vaultName: string + vaultDescription?: string + userInputtedPassword: string | undefined + storagePreference: KeySystemRootKeyStorageMode + }): Promise { + const keySystemIdentifier = UuidGenerator.GenerateUuid() + + const rootKey = await this.createKeySystemRootKey({ + keySystemIdentifier, + vaultName: dto.vaultName, + vaultDescription: dto.vaultDescription, + userInputtedPassword: dto.userInputtedPassword, + storagePreference: dto.storagePreference, + }) + + await this.createKeySystemItemsKey(keySystemIdentifier, rootKey.token) + + const vaultListing = await this.createVaultListing({ + keySystemIdentifier, + vaultName: dto.vaultName, + vaultDescription: dto.vaultDescription, + passwordType: dto.userInputtedPassword + ? KeySystemRootKeyPasswordType.UserInputted + : KeySystemRootKeyPasswordType.Randomized, + rootKeyParams: rootKey.keyParams, + storage: dto.storagePreference, + }) + + await this.sync.sync() + + return vaultListing + } + + private async createVaultListing(dto: { + keySystemIdentifier: string + vaultName: string + vaultDescription?: string + passwordType: KeySystemRootKeyPasswordType + rootKeyParams: KeySystemRootKeyParamsInterface + storage: KeySystemRootKeyStorageMode + }): Promise { + const content: VaultListingContentSpecialized = { + systemIdentifier: dto.keySystemIdentifier, + rootKeyParams: dto.rootKeyParams, + keyStorageMode: dto.storage, + name: dto.vaultName, + description: dto.vaultDescription, + } + + return this.mutator.createItem(ContentType.VaultListing, FillItemContentSpecialized(content), true) + } + + private async createKeySystemItemsKey(keySystemIdentifier: string, rootKeyToken: string): Promise { + const keySystemItemsKey = this.encryption.createKeySystemItemsKey( + UuidGenerator.GenerateUuid(), + keySystemIdentifier, + undefined, + rootKeyToken, + ) + + await this.mutator.insertItem(keySystemItemsKey) + } + + private async createKeySystemRootKey(dto: { + keySystemIdentifier: string + vaultName: string + vaultDescription?: string + userInputtedPassword: string | undefined + storagePreference: KeySystemRootKeyStorageMode + }): Promise { + let newRootKey: KeySystemRootKeyInterface | undefined + + if (dto.userInputtedPassword) { + newRootKey = this.encryption.createUserInputtedKeySystemRootKey({ + systemIdentifier: dto.keySystemIdentifier, + userInputtedPassword: dto.userInputtedPassword, + }) + } else { + newRootKey = this.encryption.createRandomizedKeySystemRootKey({ + systemIdentifier: dto.keySystemIdentifier, + }) + } + + if (dto.storagePreference === KeySystemRootKeyStorageMode.Synced) { + await this.mutator.insertItem(newRootKey, true) + } else { + this.encryption.keys.intakeNonPersistentKeySystemRootKey(newRootKey, dto.storagePreference) + } + + return newRootKey + } +} diff --git a/packages/services/src/Domain/Vaults/UseCase/DeleteVault.ts b/packages/services/src/Domain/Vaults/UseCase/DeleteVault.ts new file mode 100644 index 000000000..1a9c2b146 --- /dev/null +++ b/packages/services/src/Domain/Vaults/UseCase/DeleteVault.ts @@ -0,0 +1,32 @@ +import { ClientDisplayableError } from '@standardnotes/responses' +import { ItemManagerInterface } from '../../Item/ItemManagerInterface' +import { VaultListingInterface } from '@standardnotes/models' +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface' + +export class DeleteVaultUseCase { + constructor( + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private encryption: EncryptionProviderInterface, + ) {} + + async execute(vault: VaultListingInterface): Promise { + if (!vault.systemIdentifier) { + throw new Error('Vault system identifier is missing') + } + + await this.encryption.keys.deleteNonPersistentSystemRootKeysForVault(vault.systemIdentifier) + + const rootKeys = this.encryption.keys.getSyncedKeySystemRootKeysForVault(vault.systemIdentifier) + await this.mutator.setItemsToBeDeleted(rootKeys) + + const itemsKeys = this.encryption.keys.getKeySystemItemsKeys(vault.systemIdentifier) + await this.mutator.setItemsToBeDeleted(itemsKeys) + + const vaultItems = this.items.itemsBelongingToKeySystem(vault.systemIdentifier) + await this.mutator.setItemsToBeDeleted(vaultItems) + + await this.mutator.setItemToBeDeleted(vault) + } +} diff --git a/packages/services/src/Domain/Vaults/UseCase/GetVault.ts b/packages/services/src/Domain/Vaults/UseCase/GetVault.ts new file mode 100644 index 000000000..de8298b0d --- /dev/null +++ b/packages/services/src/Domain/Vaults/UseCase/GetVault.ts @@ -0,0 +1,17 @@ +import { VaultListingInterface } from '@standardnotes/models' +import { ItemManagerInterface } from './../../Item/ItemManagerInterface' +import { ContentType } from '@standardnotes/common' + +export class GetVaultUseCase { + constructor(private items: ItemManagerInterface) {} + + execute(query: { keySystemIdentifier: string } | { sharedVaultUuid: string }): T | undefined { + const vaults = this.items.getItems(ContentType.VaultListing) + + if ('keySystemIdentifier' in query) { + return vaults.find((listing) => listing.systemIdentifier === query.keySystemIdentifier) as T + } else { + return vaults.find((listing) => listing.sharing?.sharedVaultUuid === query.sharedVaultUuid) as T + } + } +} diff --git a/packages/services/src/Domain/Vaults/UseCase/MoveItemsToVault.ts b/packages/services/src/Domain/Vaults/UseCase/MoveItemsToVault.ts new file mode 100644 index 000000000..433a14105 --- /dev/null +++ b/packages/services/src/Domain/Vaults/UseCase/MoveItemsToVault.ts @@ -0,0 +1,42 @@ +import { MutatorClientInterface, SyncServiceInterface } from '@standardnotes/services' +import { ClientDisplayableError } from '@standardnotes/responses' +import { DecryptedItemInterface, FileItem, VaultListingInterface } from '@standardnotes/models' +import { FilesClientInterface } from '@standardnotes/files' +import { ContentType } from '@standardnotes/common' + +export class MoveItemsToVaultUseCase { + constructor( + private mutator: MutatorClientInterface, + private sync: SyncServiceInterface, + private files: FilesClientInterface, + ) {} + + async execute(dto: { + items: DecryptedItemInterface[] + vault: VaultListingInterface + }): Promise { + for (const item of dto.items) { + await this.mutator.changeItem(item, (mutator) => { + mutator.key_system_identifier = dto.vault.systemIdentifier + mutator.shared_vault_uuid = dto.vault.isSharedVaultListing() ? dto.vault.sharing.sharedVaultUuid : undefined + }) + } + + await this.sync.sync() + + for (const item of dto.items) { + if (item.content_type !== ContentType.File) { + continue + } + + if (dto.vault.isSharedVaultListing()) { + await this.files.moveFileToSharedVault(item as FileItem, dto.vault) + } else { + const itemPreviouslyBelongedToSharedVault = item.shared_vault_uuid + if (itemPreviouslyBelongedToSharedVault) { + await this.files.moveFileOutOfSharedVault(item as FileItem) + } + } + } + } +} diff --git a/packages/services/src/Domain/Vaults/UseCase/RemoveItemFromVault.ts b/packages/services/src/Domain/Vaults/UseCase/RemoveItemFromVault.ts new file mode 100644 index 000000000..44ca73529 --- /dev/null +++ b/packages/services/src/Domain/Vaults/UseCase/RemoveItemFromVault.ts @@ -0,0 +1,26 @@ +import { MutatorClientInterface, SyncServiceInterface } from '@standardnotes/services' +import { ClientDisplayableError } from '@standardnotes/responses' +import { DecryptedItemInterface, FileItem } from '@standardnotes/models' +import { ContentType } from '@standardnotes/common' +import { FilesClientInterface } from '@standardnotes/files' + +export class RemoveItemFromVault { + constructor( + private mutator: MutatorClientInterface, + private sync: SyncServiceInterface, + private files: FilesClientInterface, + ) {} + + async execute(dto: { item: DecryptedItemInterface }): Promise { + await this.mutator.changeItem(dto.item, (mutator) => { + mutator.key_system_identifier = undefined + mutator.shared_vault_uuid = undefined + }) + + await this.sync.sync() + + if (dto.item.content_type === ContentType.File) { + await this.files.moveFileOutOfSharedVault(dto.item as FileItem) + } + } +} diff --git a/packages/services/src/Domain/Vaults/UseCase/RotateVaultRootKey.ts b/packages/services/src/Domain/Vaults/UseCase/RotateVaultRootKey.ts new file mode 100644 index 000000000..6b10ff6de --- /dev/null +++ b/packages/services/src/Domain/Vaults/UseCase/RotateVaultRootKey.ts @@ -0,0 +1,90 @@ +import { UuidGenerator, assert } from '@standardnotes/utils' +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { ClientDisplayableError, isClientDisplayableError } from '@standardnotes/responses' +import { + KeySystemIdentifier, + KeySystemRootKeyInterface, + KeySystemRootKeyPasswordType, + KeySystemRootKeyStorageMode, + VaultListingInterface, + VaultListingMutator, +} from '@standardnotes/models' +import { MutatorClientInterface } from '../../Mutator/MutatorClientInterface' + +export class RotateVaultRootKeyUseCase { + constructor(private mutator: MutatorClientInterface, private encryption: EncryptionProviderInterface) {} + + async execute(params: { + vault: VaultListingInterface + sharedVaultUuid: string | undefined + userInputtedPassword: string | undefined + }): Promise { + const currentRootKey = this.encryption.keys.getPrimaryKeySystemRootKey(params.vault.systemIdentifier) + if (!currentRootKey) { + throw new Error('Cannot rotate key system root key; key system root key not found') + } + + let newRootKey: KeySystemRootKeyInterface | undefined + + if (currentRootKey.keyParams.passwordType === KeySystemRootKeyPasswordType.UserInputted) { + if (!params.userInputtedPassword) { + throw new Error('Cannot rotate key system root key; user inputted password required') + } + + newRootKey = this.encryption.createUserInputtedKeySystemRootKey({ + systemIdentifier: params.vault.systemIdentifier, + userInputtedPassword: params.userInputtedPassword, + }) + } else if (currentRootKey.keyParams.passwordType === KeySystemRootKeyPasswordType.Randomized) { + newRootKey = this.encryption.createRandomizedKeySystemRootKey({ + systemIdentifier: params.vault.systemIdentifier, + }) + } + + if (!newRootKey) { + throw new Error('Cannot rotate key system root key; new root key not created') + } + + if (params.vault.keyStorageMode === KeySystemRootKeyStorageMode.Synced) { + await this.mutator.insertItem(newRootKey, true) + } else { + this.encryption.keys.intakeNonPersistentKeySystemRootKey(newRootKey, params.vault.keyStorageMode) + } + + await this.mutator.changeItem(params.vault, (mutator) => { + assert(newRootKey) + mutator.rootKeyParams = newRootKey.keyParams + }) + + const errors: ClientDisplayableError[] = [] + + const updateKeySystemItemsKeyResult = await this.createNewKeySystemItemsKey({ + keySystemIdentifier: params.vault.systemIdentifier, + sharedVaultUuid: params.sharedVaultUuid, + rootKeyToken: newRootKey.token, + }) + + if (isClientDisplayableError(updateKeySystemItemsKeyResult)) { + errors.push(updateKeySystemItemsKeyResult) + } + + await this.encryption.reencryptKeySystemItemsKeysForVault(params.vault.systemIdentifier) + + return errors + } + + private async createNewKeySystemItemsKey(params: { + keySystemIdentifier: KeySystemIdentifier + sharedVaultUuid: string | undefined + rootKeyToken: string + }): Promise { + const newItemsKeyUuid = UuidGenerator.GenerateUuid() + const newItemsKey = this.encryption.createKeySystemItemsKey( + newItemsKeyUuid, + params.keySystemIdentifier, + params.sharedVaultUuid, + params.rootKeyToken, + ) + await this.mutator.insertItem(newItemsKey) + } +} diff --git a/packages/services/src/Domain/Vaults/VaultService.ts b/packages/services/src/Domain/Vaults/VaultService.ts new file mode 100644 index 000000000..b12e1c8d5 --- /dev/null +++ b/packages/services/src/Domain/Vaults/VaultService.ts @@ -0,0 +1,322 @@ +import { isClientDisplayableError } from '@standardnotes/responses' +import { + DecryptedItemInterface, + FileItem, + KeySystemIdentifier, + KeySystemRootKeyPasswordType, + KeySystemRootKeyStorageMode, + VaultListingInterface, + VaultListingMutator, + isNote, +} from '@standardnotes/models' +import { VaultServiceInterface } from './VaultServiceInterface' +import { ChangeVaultOptionsDTO } from './ChangeVaultOptionsDTO' +import { VaultServiceEvent, VaultServiceEventPayload } from './VaultServiceEvent' +import { EncryptionProviderInterface } from '@standardnotes/encryption' +import { CreateVaultUseCase } from './UseCase/CreateVault' +import { AbstractService } from '../Service/AbstractService' +import { SyncServiceInterface } from '../Sync/SyncServiceInterface' +import { ItemManagerInterface } from '../Item/ItemManagerInterface' +import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' +import { RemoveItemFromVault } from './UseCase/RemoveItemFromVault' +import { DeleteVaultUseCase } from './UseCase/DeleteVault' +import { MoveItemsToVaultUseCase } from './UseCase/MoveItemsToVault' + +import { RotateVaultRootKeyUseCase } from './UseCase/RotateVaultRootKey' +import { FilesClientInterface } from '@standardnotes/files' +import { ContentType } from '@standardnotes/common' +import { GetVaultUseCase } from './UseCase/GetVault' +import { ChangeVaultKeyOptionsUseCase } from './UseCase/ChangeVaultKeyOptions' +import { MutatorClientInterface } from '../Mutator/MutatorClientInterface' +import { AlertService } from '../Alert/AlertService' + +export class VaultService + extends AbstractService + implements VaultServiceInterface +{ + private lockMap = new Map() + + constructor( + private sync: SyncServiceInterface, + private items: ItemManagerInterface, + private mutator: MutatorClientInterface, + private encryption: EncryptionProviderInterface, + private files: FilesClientInterface, + private alerts: AlertService, + eventBus: InternalEventBusInterface, + ) { + super(eventBus) + + items.addObserver([ContentType.KeySystemItemsKey, ContentType.KeySystemRootKey, ContentType.VaultListing], () => { + void this.recomputeAllVaultsLockingState() + }) + } + + getVaults(): VaultListingInterface[] { + return this.items.getItems(ContentType.VaultListing).sort((a, b) => { + return a.name.localeCompare(b.name) + }) + } + + getLockedvaults(): VaultListingInterface[] { + const vaults = this.getVaults() + return vaults.filter((vault) => this.isVaultLocked(vault)) + } + + public getVault(dto: { keySystemIdentifier: KeySystemIdentifier }): VaultListingInterface | undefined { + const usecase = new GetVaultUseCase(this.items) + return usecase.execute(dto) + } + + public getSureVault(dto: { keySystemIdentifier: KeySystemIdentifier }): VaultListingInterface { + const vault = this.getVault(dto) + if (!vault) { + throw new Error('Vault not found') + } + + return vault + } + + async createRandomizedVault(dto: { + name: string + description?: string + storagePreference: KeySystemRootKeyStorageMode + }): Promise { + return this.createVaultWithParameters({ + name: dto.name, + description: dto.description, + userInputtedPassword: undefined, + storagePreference: dto.storagePreference, + }) + } + + async createUserInputtedPasswordVault(dto: { + name: string + description?: string + userInputtedPassword: string + storagePreference: KeySystemRootKeyStorageMode + }): Promise { + return this.createVaultWithParameters(dto) + } + + private async createVaultWithParameters(dto: { + name: string + description?: string + userInputtedPassword: string | undefined + storagePreference: KeySystemRootKeyStorageMode + }): Promise { + const createVault = new CreateVaultUseCase(this.mutator, this.encryption, this.sync) + const result = await createVault.execute({ + vaultName: dto.name, + vaultDescription: dto.description, + userInputtedPassword: dto.userInputtedPassword, + storagePreference: dto.storagePreference, + }) + + return result + } + + async moveItemToVault( + vault: VaultListingInterface, + item: DecryptedItemInterface, + ): Promise { + if (this.isVaultLocked(vault)) { + throw new Error('Attempting to add item to locked vault') + } + + let linkedFiles: FileItem[] = [] + if (isNote(item)) { + linkedFiles = this.items.getNoteLinkedFiles(item) + + if (linkedFiles.length > 0) { + const confirmed = await this.alerts.confirmV2({ + title: 'Linked files will be moved to vault', + text: `This note has ${linkedFiles.length} linked files. They will also be moved to the vault. Do you want to continue?`, + }) + if (!confirmed) { + return undefined + } + } + } + + const useCase = new MoveItemsToVaultUseCase(this.mutator, this.sync, this.files) + await useCase.execute({ vault, items: [item, ...linkedFiles] }) + + return this.items.findSureItem(item.uuid) + } + + async removeItemFromVault(item: DecryptedItemInterface): Promise { + const vault = this.getItemVault(item) + if (!vault) { + throw new Error('Cannot find vault to remove item from') + } + + if (this.isVaultLocked(vault)) { + throw new Error('Attempting to remove item from locked vault') + } + + const useCase = new RemoveItemFromVault(this.mutator, this.sync, this.files) + await useCase.execute({ item }) + return this.items.findSureItem(item.uuid) + } + + async deleteVault(vault: VaultListingInterface): Promise { + if (vault.isSharedVaultListing()) { + throw new Error('Shared vault must be deleted through SharedVaultService') + } + + const useCase = new DeleteVaultUseCase(this.items, this.mutator, this.encryption) + const error = await useCase.execute(vault) + + if (isClientDisplayableError(error)) { + return false + } + + await this.sync.sync() + return true + } + + async changeVaultNameAndDescription( + vault: VaultListingInterface, + params: { name: string; description?: string }, + ): Promise { + const updatedVault = await this.mutator.changeItem(vault, (mutator) => { + mutator.name = params.name + mutator.description = params.description + }) + + await this.sync.sync() + + return updatedVault + } + + async rotateVaultRootKey(vault: VaultListingInterface): Promise { + if (this.computeVaultLockState(vault) === 'locked') { + throw new Error('Cannot rotate root key of locked vault') + } + + const useCase = new RotateVaultRootKeyUseCase(this.mutator, this.encryption) + await useCase.execute({ + vault, + sharedVaultUuid: vault.isSharedVaultListing() ? vault.sharing.sharedVaultUuid : undefined, + userInputtedPassword: undefined, + }) + + await this.notifyEventSync(VaultServiceEvent.VaultRootKeyRotated, { vault }) + + await this.sync.sync() + } + + isItemInVault(item: DecryptedItemInterface): boolean { + return item.key_system_identifier !== undefined + } + + getItemVault(item: DecryptedItemInterface): VaultListingInterface | undefined { + const latestItem = this.items.findItem(item.uuid) + if (!latestItem) { + throw new Error('Cannot find latest version of item to get vault for') + } + + if (!latestItem.key_system_identifier) { + return undefined + } + + return this.getVault({ keySystemIdentifier: latestItem.key_system_identifier }) + } + + async changeVaultOptions(dto: ChangeVaultOptionsDTO): Promise { + if (this.isVaultLocked(dto.vault)) { + throw new Error('Attempting to change vault options on a locked vault') + } + + const usecase = new ChangeVaultKeyOptionsUseCase(this.items, this.mutator, this.sync, this.encryption) + await usecase.execute(dto) + + if (dto.newPasswordType) { + await this.notifyEventSync(VaultServiceEvent.VaultRootKeyRotated, { vault: dto.vault }) + } + } + + public isVaultLocked(vault: VaultListingInterface): boolean { + return this.lockMap.get(vault.uuid) === true + } + + public async lockNonPersistentVault(vault: VaultListingInterface): Promise { + if (vault.keyStorageMode === KeySystemRootKeyStorageMode.Synced) { + throw new Error('Vault uses synced root key and cannot be locked') + } + + this.encryption.keys.clearMemoryOfKeysRelatedToVault(vault) + + this.lockMap.set(vault.uuid, true) + void this.notifyEventSync(VaultServiceEvent.VaultLocked, { vault }) + } + + public async unlockNonPersistentVault(vault: VaultListingInterface, password: string): Promise { + if (vault.keyPasswordType !== KeySystemRootKeyPasswordType.UserInputted) { + throw new Error('Vault uses randomized password and cannot be unlocked with user inputted password') + } + + if (vault.keyStorageMode === KeySystemRootKeyStorageMode.Synced) { + throw new Error('Vault uses synced root key and cannot be unlocked with user inputted password') + } + + const derivedRootKey = this.encryption.deriveUserInputtedKeySystemRootKey({ + keyParams: vault.rootKeyParams, + userInputtedPassword: password, + }) + + this.encryption.keys.intakeNonPersistentKeySystemRootKey(derivedRootKey, vault.keyStorageMode) + + await this.encryption.decryptErroredPayloads() + + if (this.computeVaultLockState(vault) === 'locked') { + this.encryption.keys.undoIntakeNonPersistentKeySystemRootKey(vault.systemIdentifier) + return false + } + + this.lockMap.set(vault.uuid, false) + void this.notifyEventSync(VaultServiceEvent.VaultUnlocked, { vault }) + + return true + } + + private recomputeAllVaultsLockingState = async (): Promise => { + const vaults = this.getVaults() + + for (const vault of vaults) { + const locked = this.computeVaultLockState(vault) === 'locked' + + if (this.lockMap.get(vault.uuid) !== locked) { + this.lockMap.set(vault.uuid, locked) + + if (locked) { + void this.notifyEvent(VaultServiceEvent.VaultLocked, { vault }) + } else { + void this.notifyEvent(VaultServiceEvent.VaultUnlocked, { vault }) + } + } + } + } + + private computeVaultLockState(vault: VaultListingInterface): 'locked' | 'unlocked' { + const rootKey = this.encryption.keys.getPrimaryKeySystemRootKey(vault.systemIdentifier) + if (!rootKey) { + return 'locked' + } + + const itemsKey = this.encryption.keys.getPrimaryKeySystemItemsKey(vault.systemIdentifier) + if (!itemsKey) { + return 'locked' + } + + return 'unlocked' + } + + override deinit(): void { + super.deinit() + ;(this.sync as unknown) = undefined + ;(this.encryption as unknown) = undefined + ;(this.items as unknown) = undefined + } +} diff --git a/packages/services/src/Domain/Vaults/VaultServiceEvent.ts b/packages/services/src/Domain/Vaults/VaultServiceEvent.ts new file mode 100644 index 000000000..4d18f146c --- /dev/null +++ b/packages/services/src/Domain/Vaults/VaultServiceEvent.ts @@ -0,0 +1,19 @@ +import { VaultListingInterface } from '@standardnotes/models' + +export enum VaultServiceEvent { + VaultRootKeyRotated = 'VaultRootKeyRotated', + VaultUnlocked = 'VaultUnlocked', + VaultLocked = 'VaultLocked', +} + +export type VaultServiceEventPayload = { + [VaultServiceEvent.VaultRootKeyRotated]: { + vault: VaultListingInterface + } + [VaultServiceEvent.VaultUnlocked]: { + vault: VaultListingInterface + } + [VaultServiceEvent.VaultLocked]: { + vault: VaultListingInterface + } +} diff --git a/packages/services/src/Domain/Vaults/VaultServiceInterface.ts b/packages/services/src/Domain/Vaults/VaultServiceInterface.ts new file mode 100644 index 000000000..e9eddc255 --- /dev/null +++ b/packages/services/src/Domain/Vaults/VaultServiceInterface.ts @@ -0,0 +1,47 @@ +import { + DecryptedItemInterface, + KeySystemIdentifier, + KeySystemRootKeyStorageMode, + VaultListingInterface, +} from '@standardnotes/models' +import { AbstractService } from '../Service/AbstractService' +import { VaultServiceEvent, VaultServiceEventPayload } from './VaultServiceEvent' +import { ChangeVaultOptionsDTO } from './ChangeVaultOptionsDTO' + +export interface VaultServiceInterface + extends AbstractService { + createRandomizedVault(dto: { + name: string + description?: string + storagePreference: KeySystemRootKeyStorageMode + }): Promise + createUserInputtedPasswordVault(dto: { + name: string + description?: string + userInputtedPassword: string + storagePreference: KeySystemRootKeyStorageMode + }): Promise + + getVaults(): VaultListingInterface[] + getVault(dto: { keySystemIdentifier: KeySystemIdentifier }): VaultListingInterface | undefined + getLockedvaults(): VaultListingInterface[] + deleteVault(vault: VaultListingInterface): Promise + + moveItemToVault( + vault: VaultListingInterface, + item: DecryptedItemInterface, + ): Promise + removeItemFromVault(item: DecryptedItemInterface): Promise + isItemInVault(item: DecryptedItemInterface): boolean + getItemVault(item: DecryptedItemInterface): VaultListingInterface | undefined + + changeVaultNameAndDescription( + vault: VaultListingInterface, + params: { name: string; description: string }, + ): Promise + rotateVaultRootKey(vault: VaultListingInterface): Promise + changeVaultOptions(dto: ChangeVaultOptionsDTO): Promise + + isVaultLocked(vault: VaultListingInterface): boolean + unlockNonPersistentVault(vault: VaultListingInterface, password: string): Promise +} diff --git a/packages/services/src/Domain/index.ts b/packages/services/src/Domain/index.ts index 544b5cd77..325fa4bd7 100644 --- a/packages/services/src/Domain/index.ts +++ b/packages/services/src/Domain/index.ts @@ -1,21 +1,46 @@ export * from './Alert/AlertService' export * from './Api/ApiServiceInterface' + export * from './Application/AppGroupManagedApplication' export * from './Application/ApplicationInterface' export * from './Application/ApplicationStage' export * from './Application/DeinitCallback' export * from './Application/DeinitMode' export * from './Application/DeinitSource' -export * from './Application/WebApplicationInterface' + +export * from './AsymmetricMessage/AsymmetricMessageService' + export * from './Auth/AuthClientInterface' export * from './Auth/AuthManager' + export * from './Authenticator/AuthenticatorClientInterface' export * from './Authenticator/AuthenticatorManager' + export * from './Backups/BackupService' + export * from './Challenge' + export * from './Component/ComponentManagerInterface' export * from './Component/ComponentViewerError' export * from './Component/ComponentViewerInterface' + +export * from './Contacts/ContactServiceInterface' +export * from './Contacts/ContactService' + +export * from './KeySystem/KeySystemKeyManager' + +export * from './SharedVaults/SharedVaultServiceInterface' +export * from './SharedVaults/SharedVaultService' +export * from './SharedVaults/SharedVaultServiceEvent' +export * from './SharedVaults/PendingSharedVaultInviteRecord' + +export * from './Singleton/SingletonManagerInterface' + +export * from './Vaults/VaultService' +export * from './Vaults/VaultServiceInterface' +export * from './Vaults/VaultServiceEvent' +export * from './Vaults/ChangeVaultOptionsDTO' + export * from './Device/DatabaseItemMetadata' export * from './Device/DatabaseLoadOptions' export * from './Device/DatabaseLoadSorter' @@ -26,72 +51,102 @@ export * from './Device/DeviceInterface' export * from './Device/MobileDeviceInterface' export * from './Device/TypeCheck' export * from './Device/WebOrDesktopDeviceInterface' + export * from './Diagnostics/ServiceDiagnostics' -export * from './Encryption/BackupFileDecryptor' + +export * from './Encryption/DecryptBackupFileUseCase' export * from './Encryption/EncryptionService' export * from './Encryption/EncryptionServiceEvent' export * from './Encryption/Functions' export * from './Encryption/ItemsEncryption' export * from './Encryption/RootKeyEncryption' + export * from './Event/ApplicationEvent' export * from './Event/ApplicationEventCallback' export * from './Event/EventObserver' export * from './Event/SyncEvent' export * from './Event/SyncEventReceiver' export * from './Event/WebAppEvent' + export * from './Feature/FeaturesClientInterface' export * from './Feature/FeaturesEvent' export * from './Feature/FeatureStatus' export * from './Feature/OfflineSubscriptionEntitlements' export * from './Feature/SetOfflineFeaturesFunctionResponse' + export * from './Files/FileService' + export * from './History/HistoryServiceInterface' + export * from './Integrity/IntegrityApiInterface' export * from './Integrity/IntegrityEvent' export * from './Integrity/IntegrityEventPayload' export * from './Integrity/IntegrityService' + export * from './Internal/InternalEventBus' export * from './Internal/InternalEventBusInterface' export * from './Internal/InternalEventHandlerInterface' export * from './Internal/InternalEventInterface' export * from './Internal/InternalEventPublishStrategy' export * from './Internal/InternalEventType' -export * from './Item/ItemCounter' -export * from './Item/ItemCounterInterface' + +export * from './InternalFeatures/InternalFeature' +export * from './InternalFeatures/InternalFeatureService' +export * from './InternalFeatures/InternalFeatureServiceInterface' + +export * from './Item/StaticItemCounter' export * from './Item/ItemManagerInterface' export * from './Item/ItemRelationshipDirection' -export * from './Item/ItemsClientInterface' export * from './Item/ItemsServerInterface' + export * from './Mutator/MutatorClientInterface' +export * from './Mutator/ImportDataUseCase' + export * from './Payloads/PayloadManagerInterface' + export * from './Preferences/PreferenceServiceInterface' + export * from './Protection/MobileUnlockTiming' export * from './Protection/ProtectionClientInterface' export * from './Protection/TimingDisplayOption' + export * from './Revision/RevisionClientInterface' export * from './Revision/RevisionManager' + export * from './Service/AbstractService' export * from './Service/ServiceInterface' + export * from './Session/SessionManagerResponse' export * from './Session/SessionsClientInterface' +export * from './Session/SessionEvent' +export * from './Session/UserKeyPairChangedEventData' + export * from './Status/StatusService' export * from './Status/StatusServiceInterface' + export * from './Storage/InMemoryStore' export * from './Storage/KeyValueStoreInterface' export * from './Storage/StorageKeys' export * from './Storage/StorageServiceInterface' export * from './Storage/StorageTypes' + export * from './Strings/InfoStrings' export * from './Strings/Messages' + export * from './Subscription/AppleIAPProductId' export * from './Subscription/AppleIAPReceipt' export * from './Subscription/SubscriptionClientInterface' export * from './Subscription/SubscriptionManager' + export * from './Sync/SyncMode' export * from './Sync/SyncOptions' export * from './Sync/SyncQueueStrategy' export * from './Sync/SyncServiceInterface' export * from './Sync/SyncSource' + export * from './User/UserClientInterface' export * from './User/UserClientInterface' export * from './User/UserService' + +export * from './UserEvent/UserEventService' +export * from './UserEvent/UserEventServiceEvent' diff --git a/packages/services/tsconfig.json b/packages/services/tsconfig.json index 39b1efa35..bc3bb930b 100644 --- a/packages/services/tsconfig.json +++ b/packages/services/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "skipLibCheck": true, "rootDir": "./src", - "outDir": "./dist" + "outDir": "./dist", + "noEmit": true }, "include": ["src/**/*"], "exclude": ["**/*.spec.ts", "node_modules"] diff --git a/packages/sncrypto-common/src/Common/PureCryptoInterface.ts b/packages/sncrypto-common/src/Common/PureCryptoInterface.ts index 595b1656e..e9d237501 100644 --- a/packages/sncrypto-common/src/Common/PureCryptoInterface.ts +++ b/packages/sncrypto-common/src/Common/PureCryptoInterface.ts @@ -139,7 +139,6 @@ export interface PureCryptoInterface { senderSecretKey: HexString, recipientPublicKey: HexString, ): Base64String - sodiumCryptoBoxEasyDecrypt( ciphertext: Base64String, nonce: HexString, @@ -147,7 +146,15 @@ export interface PureCryptoInterface { recipientSecretKey: HexString, ): Utf8String - sodiumCryptoBoxGenerateKeypair(): PkcKeyPair + sodiumCryptoBoxSeedKeypair(seed: HexString): PkcKeyPair + sodiumCryptoSignSeedKeypair(seed: HexString): PkcKeyPair + + sodiumCryptoSign(message: Utf8String, secretKey: HexString): Base64String + sodiumCryptoSignVerify(message: Utf8String, signature: Base64String, publicKey: HexString): boolean + + sodiumCryptoKdfDeriveFromKey(key: HexString, subkeyNumber: number, subkeyLength: number, context: string): HexString + + sodiumCryptoGenericHash(message: Utf8String, key?: HexString): HexString /** * Converts a plain string into base64 diff --git a/packages/sncrypto-common/src/Types/PkcKeyPair.ts b/packages/sncrypto-common/src/Types/PkcKeyPair.ts index 59ecd59bc..ddf5ac3b6 100644 --- a/packages/sncrypto-common/src/Types/PkcKeyPair.ts +++ b/packages/sncrypto-common/src/Types/PkcKeyPair.ts @@ -1,7 +1,6 @@ import { HexString } from './HexString' export type PkcKeyPair = { - keyType: 'curve25519' | 'ed25519' | 'x25519' privateKey: HexString publicKey: HexString } diff --git a/packages/sncrypto-common/src/Types/SodiumConstant.ts b/packages/sncrypto-common/src/Types/SodiumConstant.ts index e6648c5ea..94f6daa48 100644 --- a/packages/sncrypto-common/src/Types/SodiumConstant.ts +++ b/packages/sncrypto-common/src/Types/SodiumConstant.ts @@ -8,4 +8,7 @@ export enum SodiumConstant { CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_REKEY = 2, CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL = 3, CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_MESSAGEBYTES_MAX = 0x3fffffff80, + crypto_box_SEEDBYTES = 32, + crypto_sign_SEEDBYTES = 32, + crypto_generichash_KEYBYTES = 32, } diff --git a/packages/sncrypto-web/src/crypto.ts b/packages/sncrypto-web/src/crypto.ts index e58783863..58751aae2 100644 --- a/packages/sncrypto-web/src/crypto.ts +++ b/packages/sncrypto-web/src/crypto.ts @@ -380,13 +380,61 @@ export class SNWebCrypto implements PureCryptoInterface { return result } - public sodiumCryptoBoxGenerateKeypair(): PkcKeyPair { - const result = sodium.crypto_box_keypair() + sodiumCryptoBoxSeedKeypair(seed: HexString): PkcKeyPair { + const result = sodium.crypto_box_seed_keypair(Utils.hexStringToArrayBuffer(seed)) const publicKey = Utils.arrayBufferToHexString(result.publicKey) const privateKey = Utils.arrayBufferToHexString(result.privateKey) - return { publicKey, privateKey, keyType: result.keyType } + return { publicKey, privateKey } + } + + sodiumCryptoSignSeedKeypair(seed: HexString): PkcKeyPair { + const result = sodium.crypto_sign_seed_keypair(Utils.hexStringToArrayBuffer(seed)) + + const publicKey = Utils.arrayBufferToHexString(result.publicKey) + const privateKey = Utils.arrayBufferToHexString(result.privateKey) + + return { publicKey, privateKey } + } + + sodiumCryptoSign(message: Utf8String, secretKey: HexString): Base64String { + const result = sodium.crypto_sign_detached(message, Utils.hexStringToArrayBuffer(secretKey)) + + return Utils.arrayBufferToBase64(result) + } + + sodiumCryptoSignVerify(message: Utf8String, signature: Base64String, publicKey: HexString): boolean { + return sodium.crypto_sign_verify_detached( + Utils.base64ToArrayBuffer(signature), + message, + Utils.hexStringToArrayBuffer(publicKey), + ) + } + + sodiumCryptoKdfDeriveFromKey(key: HexString, subkeyNumber: number, subkeyLength: number, context: string): HexString { + if (context.length !== 8) { + throw new Error('Context must be 8 bytes') + } + + const result = sodium.crypto_kdf_derive_from_key( + subkeyLength, + subkeyNumber, + context, + Utils.hexStringToArrayBuffer(key), + ) + + return Utils.arrayBufferToHexString(result) + } + + sodiumCryptoGenericHash(message: string, key?: HexString): HexString { + const result = sodium.crypto_generichash( + sodium.crypto_generichash_BYTES, + message, + key ? Utils.hexStringToArrayBuffer(key) : null, + ) + + return Utils.arrayBufferToHexString(result) } /** diff --git a/packages/sncrypto-web/src/libsodium.ts b/packages/sncrypto-web/src/libsodium.ts index d39d4ff90..46e84a237 100644 --- a/packages/sncrypto-web/src/libsodium.ts +++ b/packages/sncrypto-web/src/libsodium.ts @@ -6,12 +6,19 @@ export { crypto_box_easy, crypto_box_keypair, crypto_box_open_easy, + crypto_box_seed_keypair, + crypto_generichash, + crypto_kdf_derive_from_key, crypto_pwhash_ALG_DEFAULT, crypto_pwhash, crypto_secretstream_xchacha20poly1305_init_pull, crypto_secretstream_xchacha20poly1305_init_push, crypto_secretstream_xchacha20poly1305_pull, crypto_secretstream_xchacha20poly1305_push, + crypto_sign_detached, + crypto_sign_keypair, + crypto_sign_seed_keypair, + crypto_sign_verify_detached, from_base64, from_hex, from_string, @@ -19,6 +26,7 @@ export { to_base64, to_hex, to_string, + crypto_generichash_BYTES, } from 'libsodium-wrappers' export type { StateAddress } from 'libsodium-wrappers' diff --git a/packages/sncrypto-web/test/crypto.test.js b/packages/sncrypto-web/test/crypto.test.js index 25dde0ca8..d1a8263d5 100644 --- a/packages/sncrypto-web/test/crypto.test.js +++ b/packages/sncrypto-web/test/crypto.test.js @@ -259,15 +259,17 @@ describe('crypto operations', async function () { }) it('pkc crypto_box_easy keypair generation', async function () { - const keypair = await webCrypto.sodiumCryptoBoxGenerateKeypair() + const seed = await webCrypto.generateRandomKey(32) + const keypair = await webCrypto.sodiumCryptoBoxSeedKeypair(seed) expect(keypair.keyType).to.equal('x25519') expect(keypair.publicKey.length).to.equal(64) expect(keypair.privateKey.length).to.equal(64) }) it('pkc crypto_box_easy encrypt/decrypt', async function () { - const senderKeypair = await webCrypto.sodiumCryptoBoxGenerateKeypair() - const recipientKeypair = await webCrypto.sodiumCryptoBoxGenerateKeypair() + const seed = await webCrypto.generateRandomKey(32) + const senderKeyPair = await webCrypto.sodiumCryptoBoxSeedKeypair(seed) + const recipientKeyPair = await webCrypto.sodiumCryptoBoxSeedKeypair(seed) const nonce = await webCrypto.generateRandomKey(192) const plaintext = 'hello world 🌍' @@ -275,8 +277,8 @@ describe('crypto operations', async function () { const ciphertext = await webCrypto.sodiumCryptoBoxEasyEncrypt( plaintext, nonce, - senderKeypair.privateKey, - recipientKeypair.publicKey, + senderKeyPair.privateKey, + recipientKeyPair.publicKey, ) expect(ciphertext.length).to.equal(44) @@ -284,8 +286,8 @@ describe('crypto operations', async function () { const decrypted = await webCrypto.sodiumCryptoBoxEasyDecrypt( ciphertext, nonce, - senderKeypair.publicKey, - recipientKeypair.privateKey, + senderKeyPair.publicKey, + recipientKeyPair.privateKey, ) expect(decrypted).to.equal(plaintext) diff --git a/packages/snjs/README.md b/packages/snjs/README.md index f1748fb63..c93288f29 100644 --- a/packages/snjs/README.md +++ b/packages/snjs/README.md @@ -42,27 +42,29 @@ Object.assign(window, SNLibrary); #### Prerequisites -To run a stable server environment for E2E tests that is up to date with production, clone the [self-hosted repository](https://github.com/standardnotes/self-hosted). Make sure you have everything set up configuration wise as in self-hosting docs. In particular, make sure the env files are created and proper values for keys are set up. +To run a stable server environment for E2E tests that is up to date with production, [setup a local self-hosted server](https://standardnotes.com/help/self-hosting/docker). + +Make sure you have the following value in the env vars mentioned below. It's important to have low token TTLs for the purpose of the suite. -Make sure you have the following value in the env vars mentioned below. It's important to have low token TTLs for the purpose of the suite. For the most up to date values it's best to check `self-hosted` github workflows. At the moment of writting the recommended values are: ``` -# docker/auth.env -... -ACCESS_TOKEN_AGE=4 -REFRESH_TOKEN_AGE=10 -EPHEMERAL_SESSION_AGE=300 - # .env ... -REVISIONS_FREQUENCY=5 +AUTH_SERVER_ACCESS_TOKEN_AGE=4 +AUTH_SERVER_REFRESH_TOKEN_AGE=10 +AUTH_SERVER_EPHEMERAL_SESSION_AGE=300 +SYNCING_SERVER_REVISIONS_FREQUENCY=5 ``` -#### Start Server For Tests (SELF-HOSTED) +Edit `docker-compose.yml` ports and change keypath services.server.ports[0] from port 3000 to 3123. + +If running server without docker and as individual node processes, and you need a valid subscription for a test (such as uploading files), you'll need to clone the [mock-event-publisher](https://github.com/standardnotes/mock-event-publisher) and run it locally on port 3124. In the Container.ts file, comment out any SNS_ENDPOINT related lines for running locally. + +#### Start Server For Tests In the `self-hosted` folder run: ``` -EXPOSED_PORT=3123 ./server.sh start && ./server.sh wait-for-startup +docker compose pull && docker compose up ``` Wait for the services to be up. diff --git a/packages/snjs/jest-global.ts b/packages/snjs/jest-global.ts index b07f9ca0a..75c8cbc39 100644 --- a/packages/snjs/jest-global.ts +++ b/packages/snjs/jest-global.ts @@ -1,2 +1,3 @@ //@ts-ignore global['__VERSION__'] = global['SnjsVersion'] = require('./package.json').version +global['__IS_DEV__'] = global['isDev'] = process.env.NODE_ENV !== 'production' diff --git a/packages/snjs/lib/Application/Application.ts b/packages/snjs/lib/Application/Application.ts index 2a945b3d3..c8f4dcda0 100644 --- a/packages/snjs/lib/Application/Application.ts +++ b/packages/snjs/lib/Application/Application.ts @@ -149,6 +149,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private declare subscriptionManager: SubscriptionClientInterface private declare webSocketApiService: WebSocketApiServiceInterface private declare webSocketServer: WebSocketServerInterface + private sessionManager!: InternalServices.SNSessionManager private syncService!: InternalServices.SNSyncService private challengeService!: InternalServices.ChallengeService @@ -171,6 +172,13 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private integrityService!: ExternalServices.IntegrityService private statusService!: ExternalServices.StatusService private filesBackupService?: FilesBackupService + private vaultService!: ExternalServices.VaultServiceInterface + private contactService!: ExternalServices.ContactServiceInterface + private sharedVaultService!: ExternalServices.SharedVaultServiceInterface + private userEventService!: ExternalServices.UserEventService + private asymmetricMessageService!: ExternalServices.AsymmetricMessageService + private keySystemKeyManager!: ExternalServices.KeySystemKeyManager + private declare sessionStorageMapper: MapperInterface> private declare legacySessionStorageMapper: MapperInterface> private declare authenticatorManager: AuthenticatorClientInterface @@ -313,7 +321,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this.featuresService } - public get items(): ExternalServices.ItemsClientInterface { + public get items(): ExternalServices.ItemManagerInterface { return this.itemManager } @@ -373,6 +381,18 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this.challengeService } + public get vaults(): ExternalServices.VaultServiceInterface { + return this.vaultService + } + + public get contacts(): ExternalServices.ContactServiceInterface { + return this.contactService + } + + public get sharedVaults(): ExternalServices.SharedVaultServiceInterface { + return this.sharedVaultService + } + public computePrivateUsername(username: string): Promise { return ComputePrivateUsername(this.options.crypto, username) } @@ -534,6 +554,11 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli for (const service of this.services) { await service.handleApplicationStage(stage) } + + this.internalEventBus.publish({ + type: ApplicationEvent.ApplicationStageChanged, + payload: { stage } as ExternalServices.ApplicationStageChangedEventPayload, + }) } /** @@ -587,11 +612,13 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli } else if (event === ApplicationEvent.Launched) { this.onLaunch() } + for (const observer of this.eventHandlers.slice()) { if ((observer.singleEvent && observer.singleEvent === event) || !observer.singleEvent) { await observer.callback(event, data || {}) } } + void this.migrationService.handleApplicationEvent(event) } @@ -637,6 +664,9 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli public async getAvailableSubscriptions(): Promise< Responses.AvailableSubscriptions | Responses.ClientDisplayableError > { + if (this.isThirdPartyHostUsed()) { + return ClientDisplayableError.FromString('Third party hosts do not support subscriptions.') + } return this.sessionManager.getAvailableSubscriptions() } @@ -827,8 +857,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this.diskStorageService.setValue(key, value, mode) } - public getValue(key: string, mode?: ExternalServices.StorageValueModes): unknown { - return this.diskStorageService.getValue(key, mode) + public getValue(key: string, mode?: ExternalServices.StorageValueModes): T { + return this.diskStorageService.getValue(key, mode) } public async removeValue(key: string, mode?: ExternalServices.StorageValueModes): Promise { @@ -863,7 +893,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli } } - public addChallengeObserver(challenge: Challenge, observer: InternalServices.ChallengeObserver): () => void { + public addChallengeObserver(challenge: Challenge, observer: ExternalServices.ChallengeObserver): () => void { return this.challengeService.addChallengeObserver(challenge, observer) } @@ -980,6 +1010,53 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli }) } + public async changeAndSaveItem( + itemToLookupUuidFor: DecryptedItemInterface, + mutate: (mutator: M) => void, + updateTimestamps = true, + emitSource?: Models.PayloadEmitSource, + syncOptions?: ExternalServices.SyncOptions, + ): Promise { + await this.mutator.changeItems( + [itemToLookupUuidFor], + mutate, + updateTimestamps ? Models.MutationType.UpdateUserTimestamps : Models.MutationType.NoUpdateUserTimestamps, + emitSource, + ) + await this.syncService.sync(syncOptions) + return this.itemManager.findItem(itemToLookupUuidFor.uuid) + } + + public async changeAndSaveItems( + itemsToLookupUuidsFor: DecryptedItemInterface[], + mutate: (mutator: M) => void, + updateTimestamps = true, + emitSource?: Models.PayloadEmitSource, + syncOptions?: ExternalServices.SyncOptions, + ): Promise { + await this.mutator.changeItems( + itemsToLookupUuidsFor, + mutate, + updateTimestamps ? Models.MutationType.UpdateUserTimestamps : Models.MutationType.NoUpdateUserTimestamps, + emitSource, + ) + await this.syncService.sync(syncOptions) + } + + public async importData(data: BackupFile, awaitSync = false): Promise { + const usecase = new ExternalServices.ImportDataUseCase( + this.itemManager, + this.syncService, + this.protectionService, + this.protocolService, + this.payloadManager, + this.challengeService, + this.historyManager, + ) + + return usecase.execute(data, awaitSync) + } + private async handleRevokedSession(): Promise { /** * Because multiple API requests can come back at the same time @@ -1148,9 +1225,16 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.createMappers() this.createPayloadManager() this.createItemManager() + this.createMutatorService() + this.createDiskStorageManager() + this.createUserEventService() + this.createInMemoryStorageManager() + + this.createKeySystemKeyManager() this.createProtocolService() + this.diskStorageService.provideEncryptionProvider(this.protocolService) this.createChallengeService() this.createLegacyHttpManager() @@ -1185,7 +1269,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.createFileService() this.createIntegrityService() - this.createMutatorService() + this.createListedService() this.createActionsManager() this.createAuthenticatorManager() @@ -1193,6 +1277,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.createRevisionManager() this.createUseCases() + this.createContactService() + this.createVaultService() + this.createSharedVaultService() + this.createAsymmetricMessageService() } private clearServices() { @@ -1249,6 +1337,12 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli ;(this._listRevisions as unknown) = undefined ;(this._getRevision as unknown) = undefined ;(this._deleteRevision as unknown) = undefined + ;(this.vaultService as unknown) = undefined + ;(this.contactService as unknown) = undefined + ;(this.sharedVaultService as unknown) = undefined + ;(this.userEventService as unknown) = undefined + ;(this.asymmetricMessageService as unknown) = undefined + ;(this.keySystemKeyManager as unknown) = undefined this.services = [] } @@ -1270,6 +1364,71 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli ;(this.internalEventBus as unknown) = undefined } + private createUserEventService(): void { + this.userEventService = new ExternalServices.UserEventService(this.internalEventBus) + this.services.push(this.userEventService) + } + + private createAsymmetricMessageService() { + this.asymmetricMessageService = new ExternalServices.AsymmetricMessageService( + this.httpService, + this.protocolService, + this.contacts, + this.itemManager, + this.mutator, + this.syncService, + this.internalEventBus, + ) + this.services.push(this.asymmetricMessageService) + } + + private createContactService(): void { + this.contactService = new ExternalServices.ContactService( + this.syncService, + this.itemManager, + this.mutator, + this.sessionManager, + this.options.crypto, + this.user, + this.protocolService, + this.singletonManager, + this.internalEventBus, + ) + + this.services.push(this.contactService) + } + + private createSharedVaultService(): void { + this.sharedVaultService = new ExternalServices.SharedVaultService( + this.httpService, + this.syncService, + this.itemManager, + this.mutator, + this.protocolService, + this.sessions, + this.contactService, + this.files, + this.vaults, + this.storage, + this.internalEventBus, + ) + this.services.push(this.sharedVaultService) + } + + private createVaultService(): void { + this.vaultService = new ExternalServices.VaultService( + this.syncService, + this.itemManager, + this.mutator, + this.protocolService, + this.files, + this.alertService, + this.internalEventBus, + ) + + this.services.push(this.vaultService) + } + private createListedService(): void { this.listedService = new InternalServices.ListedService( this.apiService, @@ -1278,6 +1437,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.deprecatedHttpService, this.protectionService, this.mutator, + this.sync, this.internalEventBus, ) this.services.push(this.listedService) @@ -1286,10 +1446,11 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private createFileService() { this.fileService = new FileService( this.apiService, - this.itemManager, + this.mutator, this.syncService, this.protocolService, this.challengeService, + this.httpService, this.alertService, this.options.crypto, this.internalEventBus, @@ -1315,6 +1476,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.diskStorageService, this.apiService, this.itemManager, + this.mutator, this.webSocketsService, this.settingsService, this.userService, @@ -1366,6 +1528,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli sessionManager: this.sessionManager, challengeService: this.challengeService, itemManager: this.itemManager, + mutator: this.mutator, singletonManager: this.singletonManager, featuresService: this.featuresService, environment: this.environment, @@ -1453,6 +1616,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private createComponentManager() { this.componentManagerService = new InternalServices.SNComponentManager( this.itemManager, + this.mutator, this.syncService, this.featuresService, this.preferencesService, @@ -1508,6 +1672,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private createSingletonManager() { this.singletonManager = new InternalServices.SNSingletonManager( this.itemManager, + this.mutator, this.payloadManager, this.syncService, this.internalEventBus, @@ -1531,9 +1696,11 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private createProtocolService() { this.protocolService = new EncryptionService( this.itemManager, + this.mutator, this.payloadManager, this.deviceInterface, this.diskStorageService, + this.keySystemKeyManager, this.identifier, this.options.crypto, this.internalEventBus, @@ -1548,6 +1715,17 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.services.push(this.protocolService) } + private createKeySystemKeyManager() { + this.keySystemKeyManager = new ExternalServices.KeySystemKeyManager( + this.itemManager, + this.mutator, + this.storage, + this.internalEventBus, + ) + + this.services.push(this.keySystemKeyManager) + } + private createKeyRecoveryService() { this.keyRecoveryService = new InternalServices.SNKeyRecoveryService( this.itemManager, @@ -1582,7 +1760,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.serviceObservers.push( this.sessionManager.addEventObserver(async (event) => { switch (event) { - case InternalServices.SessionEvent.Restored: { + case ExternalServices.SessionEvent.Restored: { void (async () => { await this.sync.sync({ sourceDescription: 'Session restored pre key creation' }) if (this.protocolService.needsNewRootKeyBasedItemsKey()) { @@ -1593,10 +1771,12 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli })() break } - case InternalServices.SessionEvent.Revoked: { + case ExternalServices.SessionEvent.Revoked: { await this.handleRevokedSession() break } + case ExternalServices.SessionEvent.UserKeyPairChanged: + break default: { Utils.assertUnreachable(event) } @@ -1655,6 +1835,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private createProtectionService() { this.protectionService = new InternalServices.SNProtectionService( this.protocolService, + this.mutator, this.challengeService, this.diskStorageService, this.internalEventBus, @@ -1701,6 +1882,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.preferencesService = new InternalServices.SNPreferencesService( this.singletonManager, this.itemManager, + this.mutator, this.syncService, this.internalEventBus, ) @@ -1734,13 +1916,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private createMutatorService() { this.mutatorService = new InternalServices.MutatorService( this.itemManager, - this.syncService, - this.protectionService, - this.protocolService, this.payloadManager, - this.challengeService, - this.componentManagerService, - this.historyManager, + this.alertService, this.internalEventBus, ) this.services.push(this.mutatorService) diff --git a/packages/snjs/lib/Application/Event.ts b/packages/snjs/lib/Application/Event.ts index 91ba03516..819ba8160 100644 --- a/packages/snjs/lib/Application/Event.ts +++ b/packages/snjs/lib/Application/Event.ts @@ -5,7 +5,7 @@ export function applicationEventForSyncEvent(syncEvent: SyncEvent) { return ( { [SyncEvent.SyncCompletedWithAllItemsUploaded]: ApplicationEvent.CompletedFullSync, - [SyncEvent.SingleRoundTripSyncCompleted]: ApplicationEvent.CompletedIncrementalSync, + [SyncEvent.PaginatedSyncRequestCompleted]: ApplicationEvent.CompletedIncrementalSync, [SyncEvent.SyncError]: ApplicationEvent.FailedSync, [SyncEvent.SyncTakingTooLong]: ApplicationEvent.HighLatencySync, [SyncEvent.EnterOutOfSync]: ApplicationEvent.EnteredOutOfSync, @@ -14,7 +14,7 @@ export function applicationEventForSyncEvent(syncEvent: SyncEvent) { [SyncEvent.MajorDataChange]: ApplicationEvent.MajorDataChange, [SyncEvent.LocalDataIncrementalLoad]: ApplicationEvent.LocalDataIncrementalLoad, [SyncEvent.StatusChanged]: ApplicationEvent.SyncStatusChanged, - [SyncEvent.SyncWillBegin]: ApplicationEvent.WillSync, + [SyncEvent.SyncDidBeginProcessing]: ApplicationEvent.WillSync, [SyncEvent.InvalidSession]: ApplicationEvent.InvalidSyncSession, [SyncEvent.DatabaseReadError]: ApplicationEvent.LocalDatabaseReadError, [SyncEvent.DatabaseWriteError]: ApplicationEvent.LocalDatabaseWriteError, diff --git a/packages/snjs/lib/Application/LiveItem.ts b/packages/snjs/lib/Application/LiveItem.ts index 0b84be86b..e159d685f 100644 --- a/packages/snjs/lib/Application/LiveItem.ts +++ b/packages/snjs/lib/Application/LiveItem.ts @@ -1,12 +1,12 @@ import { DecryptedItemInterface } from '@standardnotes/models' -import { SNApplication } from './Application' +import { ApplicationInterface } from '@standardnotes/services' /** Keeps an item reference up to date with changes */ export class LiveItem { public item: T private removeObserver: () => void - constructor(uuid: string, application: SNApplication, onChange?: (item: T) => void) { + constructor(uuid: string, application: ApplicationInterface, onChange?: (item: T) => void) { this.item = application.items.findSureItem(uuid) onChange && onChange(this.item) diff --git a/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.ts b/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.ts index a3fb37248..839dbe711 100644 --- a/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.ts +++ b/packages/snjs/lib/Domain/UseCase/GetRevision/GetRevision.ts @@ -1,3 +1,4 @@ +import { ServerItemResponse } from '@standardnotes/responses' import { RevisionClientInterface } from '@standardnotes/services' import { Result, UseCaseInterface, Uuid } from '@standardnotes/domain-core' import { @@ -50,6 +51,8 @@ export class GetRevision implements UseCaseInterface { content_type: revision.content_type as ContentType, updated_at: new Date(revision.updated_at), created_at: new Date(revision.created_at), + key_system_identifier: revision.key_system_identifier ?? undefined, + shared_vault_uuid: revision.shared_vault_uuid ?? undefined, waitingForKey: false, errorDecrypting: false, }) @@ -67,7 +70,7 @@ export class GetRevision implements UseCaseInterface { uuid: sourceItemUuid || revision.item_uuid, }) - if (!isRemotePayloadAllowed(payload)) { + if (!isRemotePayloadAllowed(payload as ServerItemResponse)) { return Result.fail(`Remote payload is disallowed: ${JSON.stringify(payload)}`) } diff --git a/packages/snjs/lib/IsDev.ts b/packages/snjs/lib/IsDev.ts new file mode 100644 index 000000000..0971e851d --- /dev/null +++ b/packages/snjs/lib/IsDev.ts @@ -0,0 +1,3 @@ +/** Declared in webpack config */ +declare const __IS_DEV__: boolean +export const isDev = __IS_DEV__ diff --git a/packages/snjs/lib/Migrations/Applicators/TagsToFolders.spec.ts b/packages/snjs/lib/Migrations/Applicators/TagsToFolders.spec.ts index bdeeca746..f0c37e878 100644 --- a/packages/snjs/lib/Migrations/Applicators/TagsToFolders.spec.ts +++ b/packages/snjs/lib/Migrations/Applicators/TagsToFolders.spec.ts @@ -1,31 +1,37 @@ import { ItemManager } from '@Lib/Services' import { TagsToFoldersMigrationApplicator } from './TagsToFolders' +import { MutatorClientInterface } from '@standardnotes/services' + +describe('folders component to hierarchy', () => { + let itemManager: ItemManager + let mutator: MutatorClientInterface + let changeItemMock: jest.Mock + let findOrCreateTagParentChainMock: jest.Mock -const itemManagerMock = (tagTitles: string[]) => { const mockTag = (title: string) => ({ title, uuid: title, parentId: undefined, }) + beforeEach(() => { + itemManager = {} as unknown as jest.Mocked - const mock = { - getItems: jest.fn().mockReturnValue(tagTitles.map(mockTag)), - findOrCreateTagParentChain: jest.fn(), - changeItem: jest.fn(), - } + mutator = {} as unknown as jest.Mocked - return mock -} + changeItemMock = mutator.changeItem = jest.fn() + findOrCreateTagParentChainMock = mutator.findOrCreateTagParentChain = jest.fn() + }) -describe('folders component to hierarchy', () => { it('should produce a valid hierarchy in the simple case', async () => { const titles = ['a', 'a.b', 'a.b.c'] + itemManager.getItems - const itemManager = itemManagerMock(titles) - await TagsToFoldersMigrationApplicator.run(itemManager as unknown as ItemManager) + itemManager.getItems = jest.fn().mockReturnValue(titles.map(mockTag)) - const findOrCreateTagParentChainCalls = itemManager.findOrCreateTagParentChain.mock.calls - const changeItemCalls = itemManager.changeItem.mock.calls + await TagsToFoldersMigrationApplicator.run(itemManager, mutator) + + const findOrCreateTagParentChainCalls = findOrCreateTagParentChainMock.mock.calls + const changeItemCalls = changeItemMock.mock.calls expect(findOrCreateTagParentChainCalls.length).toEqual(2) expect(findOrCreateTagParentChainCalls[0][0]).toEqual(['a']) @@ -39,11 +45,11 @@ describe('folders component to hierarchy', () => { it('should not touch flat hierarchies', async () => { const titles = ['a', 'x', 'y', 'z'] - const itemManager = itemManagerMock(titles) - await TagsToFoldersMigrationApplicator.run(itemManager as unknown as ItemManager) + itemManager.getItems = jest.fn().mockReturnValue(titles.map(mockTag)) + await TagsToFoldersMigrationApplicator.run(itemManager, mutator) - const findOrCreateTagParentChainCalls = itemManager.findOrCreateTagParentChain.mock.calls - const changeItemCalls = itemManager.changeItem.mock.calls + const findOrCreateTagParentChainCalls = findOrCreateTagParentChainMock.mock.calls + const changeItemCalls = changeItemMock.mock.calls expect(findOrCreateTagParentChainCalls.length).toEqual(0) @@ -53,11 +59,11 @@ describe('folders component to hierarchy', () => { it('should work despite cloned tags', async () => { const titles = ['a.b', 'c', 'a.b'] - const itemManager = itemManagerMock(titles) - await TagsToFoldersMigrationApplicator.run(itemManager as unknown as ItemManager) + itemManager.getItems = jest.fn().mockReturnValue(titles.map(mockTag)) + await TagsToFoldersMigrationApplicator.run(itemManager, mutator) - const findOrCreateTagParentChainCalls = itemManager.findOrCreateTagParentChain.mock.calls - const changeItemCalls = itemManager.changeItem.mock.calls + const findOrCreateTagParentChainCalls = findOrCreateTagParentChainMock.mock.calls + const changeItemCalls = changeItemMock.mock.calls expect(findOrCreateTagParentChainCalls.length).toEqual(2) expect(findOrCreateTagParentChainCalls[0][0]).toEqual(['a']) @@ -71,11 +77,11 @@ describe('folders component to hierarchy', () => { it('should produce a valid hierarchy cases with missing intermediate tags or unordered', async () => { const titles = ['y.2', 'w.3', 'y'] - const itemManager = itemManagerMock(titles) - await TagsToFoldersMigrationApplicator.run(itemManager as unknown as ItemManager) + itemManager.getItems = jest.fn().mockReturnValue(titles.map(mockTag)) + await TagsToFoldersMigrationApplicator.run(itemManager, mutator) - const findOrCreateTagParentChainCalls = itemManager.findOrCreateTagParentChain.mock.calls - const changeItemCalls = itemManager.changeItem.mock.calls + const findOrCreateTagParentChainCalls = findOrCreateTagParentChainMock.mock.calls + const changeItemCalls = changeItemMock.mock.calls expect(findOrCreateTagParentChainCalls.length).toEqual(2) expect(findOrCreateTagParentChainCalls[0][0]).toEqual(['w']) @@ -89,11 +95,11 @@ describe('folders component to hierarchy', () => { it('skip prefixed names', async () => { const titles = ['.something', '.something...something'] - const itemManager = itemManagerMock(titles) - await TagsToFoldersMigrationApplicator.run(itemManager as unknown as ItemManager) + itemManager.getItems = jest.fn().mockReturnValue(titles.map(mockTag)) + await TagsToFoldersMigrationApplicator.run(itemManager, mutator) - const findOrCreateTagParentChainCalls = itemManager.findOrCreateTagParentChain.mock.calls - const changeItemCalls = itemManager.changeItem.mock.calls + const findOrCreateTagParentChainCalls = findOrCreateTagParentChainMock.mock.calls + const changeItemCalls = changeItemMock.mock.calls expect(findOrCreateTagParentChainCalls.length).toEqual(0) expect(changeItemCalls.length).toEqual(0) @@ -109,11 +115,11 @@ describe('folders component to hierarchy', () => { 'something..another.thing..anyway', ] - const itemManager = itemManagerMock(titles) - await TagsToFoldersMigrationApplicator.run(itemManager as unknown as ItemManager) + itemManager.getItems = jest.fn().mockReturnValue(titles.map(mockTag)) + await TagsToFoldersMigrationApplicator.run(itemManager, mutator) - const findOrCreateTagParentChainCalls = itemManager.findOrCreateTagParentChain.mock.calls - const changeItemCalls = itemManager.changeItem.mock.calls + const findOrCreateTagParentChainCalls = findOrCreateTagParentChainMock.mock.calls + const changeItemCalls = changeItemMock.mock.calls expect(findOrCreateTagParentChainCalls.length).toEqual(1) expect(findOrCreateTagParentChainCalls[0][0]).toEqual(['a', 'b']) diff --git a/packages/snjs/lib/Migrations/Applicators/TagsToFolders.ts b/packages/snjs/lib/Migrations/Applicators/TagsToFolders.ts index 3e0ae719c..690d9b15b 100644 --- a/packages/snjs/lib/Migrations/Applicators/TagsToFolders.ts +++ b/packages/snjs/lib/Migrations/Applicators/TagsToFolders.ts @@ -1,3 +1,4 @@ +import { MutatorClientInterface } from '@standardnotes/services' import { SNTag, TagMutator, TagFolderDelimitter } from '@standardnotes/models' import { ItemManager } from '@Lib/Services' import { lastElement, sortByKey, withoutLastElement } from '@standardnotes/utils' @@ -15,7 +16,7 @@ export class TagsToFoldersMigrationApplicator { return false } - public static async run(itemManager: ItemManager): Promise { + public static async run(itemManager: ItemManager, mutator: MutatorClientInterface): Promise { const tags = itemManager.getItems(ContentType.Tag) as SNTag[] const sortedTags = sortByKey(tags, 'title') @@ -36,9 +37,9 @@ export class TagsToFoldersMigrationApplicator { return } - const parent = await itemManager.findOrCreateTagParentChain(parents) + const parent = await mutator.findOrCreateTagParentChain(parents) - await itemManager.changeItem(tag, (mutator: TagMutator) => { + await mutator.changeItem(tag, (mutator: TagMutator) => { mutator.title = newTitle if (parent) { diff --git a/packages/snjs/lib/Migrations/Base.ts b/packages/snjs/lib/Migrations/Base.ts index 210fc2694..fb3507aef 100644 --- a/packages/snjs/lib/Migrations/Base.ts +++ b/packages/snjs/lib/Migrations/Base.ts @@ -1,5 +1,10 @@ import { AnyKeyParamsContent, KeyParamsContent004 } from '@standardnotes/common' -import { EncryptedPayload, EncryptedTransferPayload, isErrorDecryptingPayload } from '@standardnotes/models' +import { + EncryptedPayload, + EncryptedTransferPayload, + isErrorDecryptingPayload, + ContentTypeUsesRootKeyEncryption, +} from '@standardnotes/models' import { PreviousSnjsVersion1_0_0, PreviousSnjsVersion2_0_0, SnjsVersion } from '../Version' import { Migration } from '@Lib/Migrations/Migration' import { @@ -16,7 +21,6 @@ import { import { assert } from '@standardnotes/utils' import { CreateReader } from './StorageReaders/Functions' import { StorageReader } from './StorageReaders/Reader' -import { ContentTypeUsesRootKeyEncryption } from '@standardnotes/encryption' /** A key that was briefly present in Snjs version 2.0.0 but removed in 2.0.1 */ const LastMigrationTimeStampKey2_0_0 = 'last_migration_timestamp' diff --git a/packages/snjs/lib/Migrations/MigrationServices.ts b/packages/snjs/lib/Migrations/MigrationServices.ts index d99fe9a91..352ed2867 100644 --- a/packages/snjs/lib/Migrations/MigrationServices.ts +++ b/packages/snjs/lib/Migrations/MigrationServices.ts @@ -1,6 +1,11 @@ import { BackupServiceInterface } from '@standardnotes/files' import { Environment, Platform } from '@standardnotes/models' -import { DeviceInterface, InternalEventBusInterface, EncryptionService } from '@standardnotes/services' +import { + DeviceInterface, + InternalEventBusInterface, + EncryptionService, + MutatorClientInterface, +} from '@standardnotes/services' import { SNSessionManager } from '../Services/Session/SessionManager' import { ApplicationIdentifier } from '@standardnotes/common' import { ItemManager } from '@Lib/Services/Items/ItemManager' @@ -15,6 +20,7 @@ export type MigrationServices = { sessionManager: SNSessionManager backups?: BackupServiceInterface itemManager: ItemManager + mutator: MutatorClientInterface singletonManager: SNSingletonManager featuresService: SNFeaturesService environment: Environment diff --git a/packages/snjs/lib/Migrations/Versions/2_20_0.ts b/packages/snjs/lib/Migrations/Versions/2_20_0.ts index 70beffec8..020cb5516 100644 --- a/packages/snjs/lib/Migrations/Versions/2_20_0.ts +++ b/packages/snjs/lib/Migrations/Versions/2_20_0.ts @@ -20,7 +20,7 @@ export class Migration2_20_0 extends Migration { for (const item of items) { this.services.itemManager.removeItemLocally(item) - await this.services.storageService.deletePayloadWithId(item.uuid) + await this.services.storageService.deletePayloadWithUuid(item.uuid) } } } diff --git a/packages/snjs/lib/Migrations/Versions/2_36_0.ts b/packages/snjs/lib/Migrations/Versions/2_36_0.ts index 62743513c..70efaff09 100644 --- a/packages/snjs/lib/Migrations/Versions/2_36_0.ts +++ b/packages/snjs/lib/Migrations/Versions/2_36_0.ts @@ -20,7 +20,7 @@ export class Migration2_36_0 extends Migration { for (const item of items) { this.services.itemManager.removeItemLocally(item) - await this.services.storageService.deletePayloadWithId(item.uuid) + await this.services.storageService.deletePayloadWithUuid(item.uuid) } } } diff --git a/packages/snjs/lib/Migrations/Versions/2_42_0.ts b/packages/snjs/lib/Migrations/Versions/2_42_0.ts index 0bf3ba07c..d69bc084f 100644 --- a/packages/snjs/lib/Migrations/Versions/2_42_0.ts +++ b/packages/snjs/lib/Migrations/Versions/2_42_0.ts @@ -24,7 +24,7 @@ export class Migration2_42_0 extends Migration { }) for (const theme of themes) { - await this.services.itemManager.setItemToBeDeleted(theme) + await this.services.mutator.setItemToBeDeleted(theme) } } } diff --git a/packages/snjs/lib/Migrations/Versions/2_7_0.ts b/packages/snjs/lib/Migrations/Versions/2_7_0.ts index 48059f090..8d20f8dba 100644 --- a/packages/snjs/lib/Migrations/Versions/2_7_0.ts +++ b/packages/snjs/lib/Migrations/Versions/2_7_0.ts @@ -26,7 +26,7 @@ export class Migration2_7_0 extends Migration { const batchMgrSingleton = this.services.singletonManager.findSingleton(ContentType.Component, batchMgrPred) if (batchMgrSingleton) { - await this.services.itemManager.setItemToBeDeleted(batchMgrSingleton) + await this.services.mutator.setItemToBeDeleted(batchMgrSingleton) } } } diff --git a/packages/snjs/lib/Services/Api/ApiService.ts b/packages/snjs/lib/Services/Api/ApiService.ts index dadc13765..126849018 100644 --- a/packages/snjs/lib/Services/Api/ApiService.ts +++ b/packages/snjs/lib/Services/Api/ApiService.ts @@ -8,7 +8,6 @@ import { ItemsServerInterface, StorageKey, ApiServiceEvent, - DiagnosticInfo, KeyValueStoreInterface, API_MESSAGE_GENERIC_SYNC_FAIL, API_MESSAGE_GENERIC_TOKEN_REFRESH_FAIL, @@ -30,8 +29,8 @@ import { API_MESSAGE_TOKEN_REFRESH_IN_PROGRESS, ApiServiceEventData, } from '@standardnotes/services' -import { FilesApiInterface } from '@standardnotes/files' -import { ServerSyncPushContextualPayload, SNFeatureRepo, FileContent } from '@standardnotes/models' +import { DownloadFileParams, FileOwnershipType, FilesApiInterface } from '@standardnotes/files' +import { ServerSyncPushContextualPayload, SNFeatureRepo } from '@standardnotes/models' import { User, HttpStatusCode, @@ -72,6 +71,8 @@ import { HttpErrorResponse, HttpSuccessResponse, isErrorResponse, + ValetTokenOperation, + MoveFileResponse, } from '@standardnotes/responses' import { LegacySession, MapperInterface, Session, SessionToken } from '@standardnotes/domain-core' import { HttpServiceInterface } from '@standardnotes/api' @@ -103,7 +104,6 @@ export class SNApiService { private session: Session | LegacySession | null public user?: User - private registering = false private authenticating = false private changing = false private refreshingSession = false @@ -210,7 +210,7 @@ export class SNApiService } private errorResponseWithFallbackMessage(response: HttpErrorResponse, message: string): HttpErrorResponse { - if (!response.data.error.message) { + if (response.data.error && !response.data.error.message) { response.data.error.message = message } @@ -369,9 +369,10 @@ export class SNApiService async sync( payloads: ServerSyncPushContextualPayload[], - lastSyncToken: string, - paginationToken: string, + lastSyncToken: string | undefined, + paginationToken: string | undefined, limit: number, + sharedVaultUuids?: string[], ): Promise> { const preprocessingError = this.preprocessingError() if (preprocessingError) { @@ -383,6 +384,7 @@ export class SNApiService [ApiEndpointParam.LastSyncToken]: lastSyncToken, [ApiEndpointParam.PaginationToken]: paginationToken, [ApiEndpointParam.SyncDlLimit]: limit, + [ApiEndpointParam.SharedVaultUuids]: sharedVaultUuids, }) const response = await this.httpService.post(path, params, this.getSessionAccessToken()) @@ -686,12 +688,12 @@ export class SNApiService }) } - public async createFileValetToken( + public async createUserFileValetToken( remoteIdentifier: string, - operation: 'write' | 'read' | 'delete', + operation: ValetTokenOperation, unencryptedFileSize?: number, ): Promise { - const url = joinPaths(this.host, Paths.v1.createFileValetToken) + const url = joinPaths(this.host, Paths.v1.createUserFileValetToken) const params: CreateValetTokenPayload = { operation, @@ -717,40 +719,60 @@ export class SNApiService return response.data?.valetToken } - public async startUploadSession(apiToken: string): Promise> { - const url = joinPaths(this.getFilesHost(), Paths.v1.startUploadSession) + public async startUploadSession( + valetToken: string, + ownershipType: FileOwnershipType, + ): Promise> { + const url = joinPaths( + this.getFilesHost(), + ownershipType === 'user' ? Paths.v1.startUploadSession : Paths.v1.startSharedVaultUploadSession, + ) return this.tokenRefreshableRequest({ verb: HttpVerb.Post, url, - customHeaders: [{ key: 'x-valet-token', value: apiToken }], + customHeaders: [{ key: 'x-valet-token', value: valetToken }], fallbackErrorMessage: Strings.Network.Files.FailedStartUploadSession, }) } - public async deleteFile(apiToken: string): Promise> { - const url = joinPaths(this.getFilesHost(), Paths.v1.deleteFile) + public async deleteFile( + valetToken: string, + ownershipType: FileOwnershipType, + ): Promise> { + const url = joinPaths( + this.getFilesHost(), + ownershipType === 'user' ? Paths.v1.deleteFile : Paths.v1.deleteSharedVaultFile, + ) return this.tokenRefreshableRequest({ verb: HttpVerb.Delete, url, - customHeaders: [{ key: 'x-valet-token', value: apiToken }], + customHeaders: [{ key: 'x-valet-token', value: valetToken }], fallbackErrorMessage: Strings.Network.Files.FailedDeleteFile, }) } - public async uploadFileBytes(apiToken: string, chunkId: number, encryptedBytes: Uint8Array): Promise { + public async uploadFileBytes( + valetToken: string, + ownershipType: FileOwnershipType, + chunkId: number, + encryptedBytes: Uint8Array, + ): Promise { if (chunkId === 0) { throw Error('chunkId must start with 1') } - const url = joinPaths(this.getFilesHost(), Paths.v1.uploadFileChunk) + const url = joinPaths( + this.getFilesHost(), + ownershipType === 'user' ? Paths.v1.uploadFileChunk : Paths.v1.uploadSharedVaultFileChunk, + ) const response = await this.tokenRefreshableRequest({ verb: HttpVerb.Post, url, rawBytes: encryptedBytes, customHeaders: [ - { key: 'x-valet-token', value: apiToken }, + { key: 'x-valet-token', value: valetToken }, { key: 'x-chunk-id', value: chunkId.toString() }, { key: 'Content-Type', value: 'application/octet-stream' }, ], @@ -764,13 +786,16 @@ export class SNApiService return response.data.success } - public async closeUploadSession(apiToken: string): Promise { - const url = joinPaths(this.getFilesHost(), Paths.v1.closeUploadSession) + public async closeUploadSession(valetToken: string, ownershipType: FileOwnershipType): Promise { + const url = joinPaths( + this.getFilesHost(), + ownershipType === 'user' ? Paths.v1.closeUploadSession : Paths.v1.closeSharedVaultUploadSession, + ) const response = await this.tokenRefreshableRequest({ verb: HttpVerb.Post, url, - customHeaders: [{ key: 'x-valet-token', value: apiToken }], + customHeaders: [{ key: 'x-valet-token', value: valetToken }], fallbackErrorMessage: Strings.Network.Files.FailedCloseUploadSession, }) @@ -781,33 +806,61 @@ export class SNApiService return response.data.success } - public getFilesDownloadUrl(): string { - return joinPaths(this.getFilesHost(), Paths.v1.downloadFileChunk) + public async moveFile(valetToken: string): Promise { + const url = joinPaths(this.getFilesHost(), Paths.v1.moveFile) + + const response = await this.tokenRefreshableRequest({ + verb: HttpVerb.Post, + url, + customHeaders: [{ key: 'x-valet-token', value: valetToken }], + fallbackErrorMessage: Strings.Network.Files.FailedCloseUploadSession, + }) + + if (isErrorResponse(response)) { + return false + } + + return response.data.success } - public async downloadFile( - file: { encryptedChunkSizes: FileContent['encryptedChunkSizes'] }, - chunkIndex = 0, - apiToken: string, - contentRangeStart: number, - onBytesReceived: (bytes: Uint8Array) => Promise, - ): Promise { - const url = this.getFilesDownloadUrl() + public getFilesDownloadUrl(ownershipType: FileOwnershipType): string { + if (ownershipType === 'user') { + return joinPaths(this.getFilesHost(), Paths.v1.downloadFileChunk) + } else if (ownershipType === 'shared-vault') { + return joinPaths(this.getFilesHost(), Paths.v1.downloadSharedVaultFileChunk) + } else { + throw Error('Invalid download type') + } + } + + public async downloadFile({ + file, + chunkIndex, + valetToken, + ownershipType, + contentRangeStart, + onBytesReceived, + }: DownloadFileParams): Promise { + const url = this.getFilesDownloadUrl(ownershipType) const pullChunkSize = file.encryptedChunkSizes[chunkIndex] - const response = await this.tokenRefreshableRequest({ + const request: HttpRequest = { verb: HttpVerb.Get, url, customHeaders: [ - { key: 'x-valet-token', value: apiToken }, + { key: 'x-valet-token', value: valetToken }, { key: 'x-chunk-size', value: pullChunkSize.toString(), }, { key: 'range', value: `bytes=${contentRangeStart}-` }, ], - fallbackErrorMessage: Strings.Network.Files.FailedDownloadFileChunk, responseType: 'arraybuffer', + } + + const response = await this.tokenRefreshableRequest({ + ...request, + fallbackErrorMessage: Strings.Network.Files.FailedDownloadFileChunk, }) if (isErrorResponse(response)) { @@ -833,7 +886,14 @@ export class SNApiService await onBytesReceived(bytesReceived) if (rangeEnd < totalSize - 1) { - return this.downloadFile(file, ++chunkIndex, apiToken, rangeStart + pullChunkSize, onBytesReceived) + return this.downloadFile({ + file, + chunkIndex: ++chunkIndex, + valetToken, + ownershipType, + contentRangeStart: rangeStart + pullChunkSize, + onBytesReceived, + }) } return undefined @@ -889,19 +949,4 @@ export class SNApiService return this.session.accessToken } - - override getDiagnostics(): Promise { - return Promise.resolve({ - api: { - hasSession: this.session != undefined, - user: this.user, - registering: this.registering, - authenticating: this.authenticating, - changing: this.changing, - refreshingSession: this.refreshingSession, - filesHost: this.filesHost, - host: this.host, - }, - }) - } } diff --git a/packages/snjs/lib/Services/Api/Paths.ts b/packages/snjs/lib/Services/Api/Paths.ts index 331987108..815b555fa 100644 --- a/packages/snjs/lib/Services/Api/Paths.ts +++ b/packages/snjs/lib/Services/Api/Paths.ts @@ -1,12 +1,22 @@ const FilesPaths = { closeUploadSession: '/v1/files/upload/close-session', - createFileValetToken: '/v1/files/valet-tokens', + createUserFileValetToken: '/v1/files/valet-tokens', deleteFile: '/v1/files', downloadFileChunk: '/v1/files', + downloadVaultFileChunk: '/v1/vaults/files', startUploadSession: '/v1/files/upload/create-session', uploadFileChunk: '/v1/files/upload/chunk', } +const SharedVaultFilesPaths = { + closeSharedVaultUploadSession: '/v1/shared-vault/files/upload/close-session', + deleteSharedVaultFile: '/v1/shared-vault/files', + downloadSharedVaultFileChunk: '/v1/shared-vault/files', + startSharedVaultUploadSession: '/v1/shared-vault/files/upload/create-session', + uploadSharedVaultFileChunk: '/v1/shared-vault/files/upload/chunk', + moveFile: '/v1/shared-vault/files/move', +} + const UserPaths = { changeCredentials: (userUuid: string) => `/v1/users/${userUuid}/attributes/credentials`, deleteAccount: (userUuid: string) => `/v1/users/${userUuid}`, @@ -58,6 +68,7 @@ const ListedPaths = { export const Paths = { v1: { ...FilesPaths, + ...SharedVaultFilesPaths, ...ItemsPaths, ...ListedPaths, ...SettingsPaths, diff --git a/packages/snjs/lib/Services/Challenge/ChallengeOperation.ts b/packages/snjs/lib/Services/Challenge/ChallengeOperation.ts index d9f06164b..c7ac66b6e 100644 --- a/packages/snjs/lib/Services/Challenge/ChallengeOperation.ts +++ b/packages/snjs/lib/Services/Challenge/ChallengeOperation.ts @@ -1,7 +1,6 @@ -import { Challenge, ChallengeValue, ChallengeArtifacts } from '@standardnotes/services' +import { Challenge, ChallengeValue, ChallengeArtifacts, ChallengeValueCallback } from '@standardnotes/services' import { ChallengeResponse } from './ChallengeResponse' import { removeFromArray } from '@standardnotes/utils' -import { ValueCallback } from './ChallengeService' /** * A challenge operation stores user-submitted values and callbacks. @@ -15,8 +14,8 @@ export class ChallengeOperation { constructor( public challenge: Challenge, - public onValidValue: ValueCallback, - public onInvalidValue: ValueCallback, + public onValidValue: ChallengeValueCallback, + public onInvalidValue: ChallengeValueCallback, public onNonvalidatedSubmit: (response: ChallengeResponse) => void, public onComplete: (response: ChallengeResponse) => void, public onCancel: () => void, diff --git a/packages/snjs/lib/Services/Challenge/ChallengeService.ts b/packages/snjs/lib/Services/Challenge/ChallengeService.ts index d22103d8c..45f841f4d 100644 --- a/packages/snjs/lib/Services/Challenge/ChallengeService.ts +++ b/packages/snjs/lib/Services/Challenge/ChallengeService.ts @@ -16,6 +16,7 @@ import { ChallengePrompt, EncryptionService, ChallengeStrings, + ChallengeObserver, } from '@standardnotes/services' import { ChallengeResponse } from './ChallengeResponse' import { ChallengeOperation } from './ChallengeOperation' @@ -25,16 +26,6 @@ type ChallengeValidationResponse = { artifacts?: ChallengeArtifacts } -export type ValueCallback = (value: ChallengeValue) => void - -export type ChallengeObserver = { - onValidValue?: ValueCallback - onInvalidValue?: ValueCallback - onNonvalidatedSubmit?: (response: ChallengeResponse) => void - onComplete?: (response: ChallengeResponse) => void - onCancel?: () => void -} - const clearChallengeObserver = (observer: ChallengeObserver) => { observer.onCancel = undefined observer.onComplete = undefined @@ -112,7 +103,7 @@ export class ChallengeService extends AbstractService implements ChallengeServic return value.value as string } - async promptForAccountPassword(): Promise { + async promptForAccountPassword(): Promise { if (!this.protocolService.hasAccount()) { throw Error('Requiring account password for challenge with no account') } @@ -126,11 +117,7 @@ export class ChallengeService extends AbstractService implements ChallengeServic ), ) - if (response) { - return true - } else { - return false - } + return response?.getValueForType(ChallengeValidation.AccountPassword)?.value as string } /** @@ -175,7 +162,7 @@ export class ChallengeService extends AbstractService implements ChallengeServic return this.protocolService.isPasscodeLocked() } - public addChallengeObserver(challenge: Challenge, observer: ChallengeObserver) { + public addChallengeObserver(challenge: Challenge, observer: ChallengeObserver): () => void { const observers = this.challengeObservers[challenge.id] || [] observers.push(observer) @@ -303,11 +290,11 @@ export class ChallengeService extends AbstractService implements ChallengeServic } public setValidationStatusForChallenge( - challenge: Challenge, + challenge: ChallengeInterface, value: ChallengeValue, valid: boolean, artifacts?: ChallengeArtifacts, - ) { + ): void { const operation = this.getChallengeOperation(challenge) operation.setValueStatus(value, valid, artifacts) diff --git a/packages/snjs/lib/Services/ComponentManager/ComponentManager.spec.ts b/packages/snjs/lib/Services/ComponentManager/ComponentManager.spec.ts index f5454bb16..5fa9a6096 100644 --- a/packages/snjs/lib/Services/ComponentManager/ComponentManager.spec.ts +++ b/packages/snjs/lib/Services/ComponentManager/ComponentManager.spec.ts @@ -19,6 +19,7 @@ import { InternalEventBusInterface, AlertService, DeviceInterface, + MutatorClientInterface, } from '@standardnotes/services' import { ItemManager } from '@Lib/Services/Items/ItemManager' import { SNFeaturesService } from '@Lib/Services/Features/FeaturesService' @@ -27,6 +28,7 @@ import { SNSyncService } from '../Sync/SyncService' describe('featuresService', () => { let itemManager: ItemManager + let mutator: MutatorClientInterface let featureService: SNFeaturesService let alertService: AlertService let syncService: SNSyncService @@ -52,6 +54,7 @@ describe('featuresService', () => { const manager = new SNComponentManager( itemManager, + mutator, syncService, featureService, prefsService, @@ -71,12 +74,14 @@ describe('featuresService', () => { itemManager = {} as jest.Mocked itemManager.getItems = jest.fn().mockReturnValue([]) - itemManager.createItem = jest.fn() - itemManager.changeComponent = jest.fn().mockReturnValue({} as jest.Mocked) - itemManager.setItemsToBeDeleted = jest.fn() itemManager.addObserver = jest.fn() - itemManager.changeItem = jest.fn() - itemManager.changeFeatureRepo = jest.fn() + + mutator = {} as jest.Mocked + mutator.createItem = jest.fn() + mutator.changeComponent = jest.fn().mockReturnValue({} as jest.Mocked) + mutator.setItemsToBeDeleted = jest.fn() + mutator.changeItem = jest.fn() + mutator.changeFeatureRepo = jest.fn() featureService = {} as jest.Mocked diff --git a/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts b/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts index ae38c6976..57fb5ea76 100644 --- a/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts +++ b/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts @@ -39,6 +39,7 @@ import { AlertService, DeviceInterface, isMobileDevice, + MutatorClientInterface, } from '@standardnotes/services' const DESKTOP_URL_PREFIX = 'sn://' @@ -78,6 +79,7 @@ export class SNComponentManager constructor( private itemManager: ItemManager, + private mutator: MutatorClientInterface, private syncService: SNSyncService, private featuresService: SNFeaturesService, private preferencesSerivce: SNPreferencesService, @@ -162,6 +164,7 @@ export class SNComponentManager const viewer = new ComponentViewer( component, this.itemManager, + this.mutator, this.syncService, this.alertService, this.preferencesSerivce, @@ -482,7 +485,7 @@ export class SNComponentManager } } - await this.itemManager.changeItem(component, (m) => { + await this.mutator.changeItem(component, (m) => { const mutator = m as ComponentMutator mutator.permissions = componentPermissions }) @@ -546,14 +549,14 @@ export class SNComponentManager const theme = this.findComponent(uuid) as SNTheme if (theme.active) { - await this.itemManager.changeComponent(theme, (mutator) => { + await this.mutator.changeComponent(theme, (mutator) => { mutator.active = false }) } else { const activeThemes = this.getActiveThemes() /* Activate current before deactivating others, so as not to flicker */ - await this.itemManager.changeComponent(theme, (mutator) => { + await this.mutator.changeComponent(theme, (mutator) => { mutator.active = true }) @@ -562,13 +565,15 @@ export class SNComponentManager await sleep(10) for (const candidate of activeThemes) { if (candidate && !candidate.isLayerable()) { - await this.itemManager.changeComponent(candidate, (mutator) => { + await this.mutator.changeComponent(candidate, (mutator) => { mutator.active = false }) } } } } + + void this.syncService.sync() } async toggleComponent(uuid: UuidString): Promise { @@ -580,9 +585,11 @@ export class SNComponentManager return } - await this.itemManager.changeComponent(component, (mutator) => { + await this.mutator.changeComponent(component, (mutator) => { mutator.active = !(mutator.getItem() as SNComponent).active }) + + void this.syncService.sync() } isComponentActive(component: SNComponent): boolean { diff --git a/packages/snjs/lib/Services/ComponentManager/ComponentViewer.ts b/packages/snjs/lib/Services/ComponentManager/ComponentViewer.ts index e900c6d72..e3182f1f7 100644 --- a/packages/snjs/lib/Services/ComponentManager/ComponentViewer.ts +++ b/packages/snjs/lib/Services/ComponentManager/ComponentViewer.ts @@ -5,6 +5,7 @@ import { FeatureStatus, FeaturesEvent, AlertService, + MutatorClientInterface, } from '@standardnotes/services' import { SNFeaturesService } from '@Lib/Services' import { @@ -109,6 +110,7 @@ export class ComponentViewer implements ComponentViewerInterface { constructor( public readonly component: SNComponent, private itemManager: ItemManager, + private mutator: MutatorClientInterface, private syncService: SNSyncService, private alertService: AlertService, private preferencesSerivce: SNPreferencesService, @@ -719,7 +721,7 @@ export class ComponentViewer implements ComponentViewerInterface { ...contextualPayload, }) const template = CreateDecryptedItemFromPayload(payload) - await this.itemManager.insertItem(template) + await this.mutator.insertItem(template) } else { if (contextualPayload.content_type !== item.content_type) { throw Error('Extension is trying to modify content type of item.') @@ -727,7 +729,7 @@ export class ComponentViewer implements ComponentViewerInterface { } } - await this.itemManager.changeItems( + await this.mutator.changeItems( items.filter(isNotUndefined), (mutator) => { const contextualPayload = sureSearchArray(contextualPayloads, { @@ -798,9 +800,9 @@ export class ComponentViewer implements ComponentViewerInterface { }) const template = CreateDecryptedItemFromPayload(payload) - const item = await this.itemManager.insertItem(template) + const item = await this.mutator.insertItem(template) - await this.itemManager.changeItem( + await this.mutator.changeItem( item, (mutator) => { if (responseItem.clientData) { @@ -857,7 +859,7 @@ export class ComponentViewer implements ComponentViewerInterface { void this.alertService.alert('The item you are trying to delete cannot be found.') continue } - await this.itemManager.setItemToBeDeleted(item, PayloadEmitSource.ComponentRetrieved) + await this.mutator.setItemToBeDeleted(item, PayloadEmitSource.ComponentRetrieved) } void this.syncService.sync() @@ -875,7 +877,7 @@ export class ComponentViewer implements ComponentViewerInterface { handleSetComponentDataMessage(message: ComponentMessage): void { const noPermissionsRequired: ComponentPermission[] = [] this.componentManagerFunctions.runWithPermissions(this.component.uuid, noPermissionsRequired, async () => { - await this.itemManager.changeComponent(this.component, (mutator) => { + await this.mutator.changeComponent(this.component, (mutator) => { mutator.componentData = message.data.componentData || {} }) diff --git a/packages/snjs/lib/Services/Features/FeaturesService.spec.ts b/packages/snjs/lib/Services/Features/FeaturesService.spec.ts index a9adfa606..7074f89f8 100644 --- a/packages/snjs/lib/Services/Features/FeaturesService.spec.ts +++ b/packages/snjs/lib/Services/Features/FeaturesService.spec.ts @@ -14,6 +14,7 @@ import { FeaturesEvent, FeatureStatus, InternalEventBusInterface, + MutatorClientInterface, StorageKey, UserService, } from '@standardnotes/services' @@ -25,6 +26,7 @@ describe('featuresService', () => { let storageService: DiskStorageService let apiService: SNApiService let itemManager: ItemManager + let mutator: MutatorClientInterface let webSocketsService: SNWebSocketsService let settingsService: SNSettingsService let userService: UserService @@ -46,6 +48,7 @@ describe('featuresService', () => { storageService, apiService, itemManager, + mutator, webSocketsService, settingsService, userService, @@ -95,13 +98,15 @@ describe('featuresService', () => { itemManager = {} as jest.Mocked itemManager.getItems = jest.fn().mockReturnValue(items) - itemManager.createItem = jest.fn() itemManager.createTemplateItem = jest.fn().mockReturnValue({}) - itemManager.changeComponent = jest.fn().mockReturnValue({} as jest.Mocked) - itemManager.setItemsToBeDeleted = jest.fn() itemManager.addObserver = jest.fn() - itemManager.changeItem = jest.fn() - itemManager.changeFeatureRepo = jest.fn() + + mutator = {} as jest.Mocked + mutator.createItem = jest.fn() + mutator.changeComponent = jest.fn().mockReturnValue({} as jest.Mocked) + mutator.setItemsToBeDeleted = jest.fn() + mutator.changeItem = jest.fn() + mutator.changeFeatureRepo = jest.fn() webSocketsService = {} as jest.Mocked webSocketsService.addEventObserver = jest.fn() @@ -173,7 +178,7 @@ describe('featuresService', () => { const { didChangeRoles } = await featuresService.updateOnlineRoles(newRoles) await featuresService.fetchFeatures('123', didChangeRoles) - expect(itemManager.createItem).not.toHaveBeenCalled() + expect(mutator.createItem).not.toHaveBeenCalled() }) it('does create a component for enabled experimental feature', async () => { @@ -196,7 +201,7 @@ describe('featuresService', () => { const { didChangeRoles } = await featuresService.updateOnlineRoles(newRoles) await featuresService.fetchFeatures('123', didChangeRoles) - expect(itemManager.createItem).toHaveBeenCalled() + expect(mutator.createItem).toHaveBeenCalled() }) }) @@ -300,8 +305,8 @@ describe('featuresService', () => { const { didChangeRoles } = await featuresService.updateOnlineRoles(newRoles) await featuresService.fetchFeatures('123', didChangeRoles) - expect(itemManager.createItem).toHaveBeenCalledTimes(2) - expect(itemManager.createItem).toHaveBeenCalledWith( + expect(mutator.createItem).toHaveBeenCalledTimes(2) + expect(mutator.createItem).toHaveBeenCalledWith( ContentType.Theme, expect.objectContaining({ package_info: expect.objectContaining({ @@ -312,7 +317,7 @@ describe('featuresService', () => { }), true, ) - expect(itemManager.createItem).toHaveBeenCalledWith( + expect(mutator.createItem).toHaveBeenCalledWith( ContentType.Component, expect.objectContaining({ package_info: expect.objectContaining({ @@ -346,7 +351,7 @@ describe('featuresService', () => { const { didChangeRoles } = await featuresService.updateOnlineRoles(newRoles) await featuresService.fetchFeatures('123', didChangeRoles) - expect(itemManager.changeComponent).toHaveBeenCalledWith(existingItem, expect.any(Function)) + expect(mutator.changeComponent).toHaveBeenCalledWith(existingItem, expect.any(Function)) }) it('creates items for expired components if they do not exist', async () => { @@ -373,7 +378,7 @@ describe('featuresService', () => { const { didChangeRoles } = await featuresService.updateOnlineRoles(newRoles) await featuresService.fetchFeatures('123', didChangeRoles) - expect(itemManager.createItem).toHaveBeenCalledWith( + expect(mutator.createItem).toHaveBeenCalledWith( ContentType.Component, expect.objectContaining({ package_info: expect.objectContaining({ @@ -403,7 +408,7 @@ describe('featuresService', () => { const now = new Date() const yesterday = now.setDate(now.getDate() - 1) - itemManager.changeComponent = jest.fn().mockReturnValue(existingItem) + mutator.changeComponent = jest.fn().mockReturnValue(existingItem) storageService.getValue = jest.fn().mockReturnValue(roles) itemManager.getItems = jest.fn().mockReturnValue([existingItem]) apiService.getUserFeatures = jest.fn().mockReturnValue({ @@ -422,7 +427,7 @@ describe('featuresService', () => { const { didChangeRoles } = await featuresService.updateOnlineRoles(newRoles) await featuresService.fetchFeatures('123', didChangeRoles) - expect(itemManager.setItemsToBeDeleted).toHaveBeenCalledWith([existingItem]) + expect(mutator.setItemsToBeDeleted).toHaveBeenCalledWith([existingItem]) }) it('does not create an item for a feature without content type', async () => { @@ -447,7 +452,7 @@ describe('featuresService', () => { const { didChangeRoles } = await featuresService.updateOnlineRoles(newRoles) await featuresService.fetchFeatures('123', didChangeRoles) - expect(itemManager.createItem).not.toHaveBeenCalled() + expect(mutator.createItem).not.toHaveBeenCalled() }) it('does not create an item for deprecated features', async () => { @@ -472,7 +477,7 @@ describe('featuresService', () => { const { didChangeRoles } = await featuresService.updateOnlineRoles(newRoles) await featuresService.fetchFeatures('123', didChangeRoles) - expect(itemManager.createItem).not.toHaveBeenCalled() + expect(mutator.createItem).not.toHaveBeenCalled() }) it('does nothing after initial update if roles have not changed', async () => { diff --git a/packages/snjs/lib/Services/Features/FeaturesService.ts b/packages/snjs/lib/Services/Features/FeaturesService.ts index bcbb997bf..aee6833bd 100644 --- a/packages/snjs/lib/Services/Features/FeaturesService.ts +++ b/packages/snjs/lib/Services/Features/FeaturesService.ts @@ -48,6 +48,7 @@ import { SetOfflineFeaturesFunctionResponse, StorageKey, UserService, + MutatorClientInterface, } from '@standardnotes/services' import { FeatureIdentifier } from '@standardnotes/features' @@ -72,7 +73,8 @@ export class SNFeaturesService private storageService: DiskStorageService, private apiService: SNApiService, private itemManager: ItemManager, - private webSocketsService: SNWebSocketsService, + private mutator: MutatorClientInterface, + webSocketsService: SNWebSocketsService, private settingsService: SNSettingsService, private userService: UserService, private syncService: SNSyncService, @@ -188,7 +190,7 @@ export class SNFeaturesService if (existingItem) { const hasChange = JSON.stringify(feature) !== JSON.stringify(existingItem.package_info) if (hasChange) { - await this.itemManager.changeComponent(existingItem, (mutator) => { + await this.mutator.changeComponent(existingItem, (mutator) => { mutator.package_info = feature }) } @@ -196,7 +198,7 @@ export class SNFeaturesService continue } - await this.itemManager.createItem( + await this.mutator.createItem( feature.content_type, this.componentContentForNativeFeatureDescription(feature), true, @@ -230,7 +232,7 @@ export class SNFeaturesService return } - void this.itemManager.setItemToBeDeleted(component).then(() => { + void this.mutator.setItemToBeDeleted(component).then(() => { void this.syncService.sync() }) void this.notifyEvent(FeaturesEvent.FeaturesUpdated) @@ -270,7 +272,7 @@ export class SNFeaturesService return result } - const offlineRepo = (await this.itemManager.createItem( + const offlineRepo = (await this.mutator.createItem( ContentType.ExtensionRepo, FillItemContent({ offlineFeaturesUrl: result.featuresUrl, @@ -298,7 +300,7 @@ export class SNFeaturesService public async deleteOfflineFeatureRepo(): Promise { const repo = this.getOfflineRepo() if (repo) { - await this.itemManager.setItemToBeDeleted(repo) + await this.mutator.setItemToBeDeleted(repo) void this.syncService.sync() } await this.storageService.removeValue(StorageKey.UserFeatures) @@ -346,7 +348,7 @@ export class SNFeaturesService userKey, true, ) - await this.itemManager.changeFeatureRepo(item, (m) => { + await this.mutator.changeFeatureRepo(item, (m) => { m.migratedToUserSetting = true }) } @@ -371,7 +373,7 @@ export class SNFeaturesService const userKeyMatch = repoUrl.match(/\w{32,64}/) if (userKeyMatch && userKeyMatch.length > 0) { const userKey = userKeyMatch[0] - const updatedRepo = await this.itemManager.changeFeatureRepo(item, (m) => { + const updatedRepo = await this.mutator.changeFeatureRepo(item, (m) => { m.offlineFeaturesUrl = PROD_OFFLINE_FEATURES_URL m.offlineKey = userKey m.migratedToOfflineEntitlements = true @@ -647,7 +649,7 @@ export class SNFeaturesService } } - await this.itemManager.setItemsToBeDeleted(itemsToDelete) + await this.mutator.setItemsToBeDeleted(itemsToDelete) if (hasChanges) { void this.syncService.sync() @@ -704,7 +706,7 @@ export class SNFeaturesService const hasChange = hasChangeInPackageInfo || hasChangeInExpiration if (hasChange) { - resultingItem = await this.itemManager.changeComponent(existingItem, (mutator) => { + resultingItem = await this.mutator.changeComponent(existingItem, (mutator) => { mutator.package_info = feature mutator.valid_until = featureExpiresAt }) @@ -714,7 +716,7 @@ export class SNFeaturesService resultingItem = existingItem } } else if (!expired || feature.content_type === ContentType.Component) { - resultingItem = (await this.itemManager.createItem( + resultingItem = (await this.mutator.createItem( feature.content_type, this.componentContentForNativeFeatureDescription(feature), true, @@ -835,7 +837,7 @@ export class SNFeaturesService ;(this.storageService as unknown) = undefined ;(this.apiService as unknown) = undefined ;(this.itemManager as unknown) = undefined - ;(this.webSocketsService as unknown) = undefined + ;(this.mutator as unknown) = undefined ;(this.settingsService as unknown) = undefined ;(this.userService as unknown) = undefined ;(this.syncService as unknown) = undefined diff --git a/packages/snjs/lib/Services/Items/ItemManager.spec.ts b/packages/snjs/lib/Services/Items/ItemManager.spec.ts index b0214aa38..bdc07e3ed 100644 --- a/packages/snjs/lib/Services/Items/ItemManager.spec.ts +++ b/packages/snjs/lib/Services/Items/ItemManager.spec.ts @@ -1,8 +1,8 @@ import { ContentType } from '@standardnotes/common' -import { InternalEventBusInterface, ItemRelationshipDirection } from '@standardnotes/services' +import { AlertService, InternalEventBusInterface, ItemRelationshipDirection } from '@standardnotes/services' import { ItemManager } from './ItemManager' import { PayloadManager } from '../Payloads/PayloadManager' -import { UuidGenerator } from '@standardnotes/utils' +import { UuidGenerator, assert } from '@standardnotes/utils' import * as Models from '@standardnotes/models' import { DecryptedPayload, @@ -15,6 +15,7 @@ import { SystemViewId, } from '@standardnotes/models' import { createNoteWithTitle } from '../../Spec/SpecUtils' +import { MutatorService } from '../Mutator' const setupRandomUuid = () => { UuidGenerator.SetGenerator(() => String(Math.random())) @@ -43,15 +44,11 @@ const LongTextPredicate = Models.predicateFromJson({ }) describe('itemManager', () => { + let mutator: MutatorService let payloadManager: PayloadManager let itemManager: ItemManager - let items: Models.DecryptedItemInterface[] let internalEventBus: InternalEventBusInterface - const createService = () => { - return new ItemManager(payloadManager, internalEventBus) - } - beforeEach(() => { setupRandomUuid() @@ -59,16 +56,9 @@ describe('itemManager', () => { internalEventBus.publish = jest.fn() payloadManager = new PayloadManager(internalEventBus) + itemManager = new ItemManager(payloadManager, internalEventBus) - items = [] as jest.Mocked - itemManager = {} as jest.Mocked - itemManager.getItems = jest.fn().mockReturnValue(items) - itemManager.createItem = jest.fn() - itemManager.changeComponent = jest.fn().mockReturnValue({} as jest.Mocked) - itemManager.setItemsToBeDeleted = jest.fn() - itemManager.addObserver = jest.fn() - itemManager.changeItem = jest.fn() - itemManager.changeFeatureRepo = jest.fn() + mutator = new MutatorService(itemManager, payloadManager, {} as jest.Mocked, internalEventBus) }) const createTag = (title: string) => { @@ -99,8 +89,6 @@ describe('itemManager', () => { describe('item emit', () => { it('deleted payloads should map to removed items', async () => { - itemManager = createService() - const payload = new DeletedPayload({ uuid: String(Math.random()), content_type: ContentType.Note, @@ -120,8 +108,6 @@ describe('itemManager', () => { }) it('decrypted items who become encrypted should be removed from ui', async () => { - itemManager = createService() - const decrypted = new DecryptedPayload({ uuid: String(Math.random()), content_type: ContentType.Note, @@ -154,11 +140,10 @@ describe('itemManager', () => { describe('note display criteria', () => { it('viewing notes with tag', async () => { - itemManager = createService() const tag = createTag('parent') const note = createNoteWithTitle('note') - await itemManager.insertItems([tag, note]) - await itemManager.addTagToNote(note, tag, false) + await mutator.insertItems([tag, note]) + await mutator.addTagToNote(note, tag, false) itemManager.setPrimaryItemDisplayOptions({ tags: [tag], @@ -171,21 +156,19 @@ describe('itemManager', () => { }) it('viewing trashed notes smart view should include archived notes', async () => { - itemManager = createService() - const archivedNote = createNoteWithTitle('archived') const trashedNote = createNoteWithTitle('trashed') const archivedAndTrashedNote = createNoteWithTitle('archived&trashed') - await itemManager.insertItems([archivedNote, trashedNote, archivedAndTrashedNote]) + await mutator.insertItems([archivedNote, trashedNote, archivedAndTrashedNote]) - await itemManager.changeItem(archivedNote, (m) => { + await mutator.changeItem(archivedNote, (m) => { m.archived = true }) - await itemManager.changeItem(trashedNote, (m) => { + await mutator.changeItem(trashedNote, (m) => { m.trashed = true }) - await itemManager.changeItem(archivedAndTrashedNote, (m) => { + await mutator.changeItem(archivedAndTrashedNote, (m) => { m.trashed = true m.archived = true }) @@ -206,58 +189,53 @@ describe('itemManager', () => { describe('tag relationships', () => { it('updates parentId of child tag', async () => { - itemManager = createService() const parent = createTag('parent') const child = createTag('child') - await itemManager.insertItems([parent, child]) - await itemManager.setTagParent(parent, child) + await mutator.insertItems([parent, child]) + await mutator.setTagParent(parent, child) const changedChild = itemManager.findItem(child.uuid) as Models.SNTag expect(changedChild.parentId).toBe(parent.uuid) }) it('forbids a tag to be its own parent', async () => { - itemManager = createService() const tag = createTag('tag') - await itemManager.insertItems([tag]) + await mutator.insertItems([tag]) - expect(() => itemManager.setTagParent(tag, tag)).toThrow() + await expect(mutator.setTagParent(tag, tag)).rejects.toThrow() expect(itemManager.getTagParent(tag)).toBeUndefined() }) it('forbids a tag to be its own ancestor', async () => { - itemManager = createService() const grandParent = createTag('grandParent') const parent = createTag('parent') const child = createTag('child') - await itemManager.insertItems([child, parent, grandParent]) - await itemManager.setTagParent(parent, child) - await itemManager.setTagParent(grandParent, parent) + await mutator.insertItems([child, parent, grandParent]) + await mutator.setTagParent(parent, child) + await mutator.setTagParent(grandParent, parent) - expect(() => itemManager.setTagParent(child, grandParent)).toThrow() + await expect(mutator.setTagParent(child, grandParent)).rejects.toThrow() expect(itemManager.getTagParent(grandParent)).toBeUndefined() }) it('getTagParent', async () => { - itemManager = createService() const parent = createTag('parent') const child = createTag('child') - await itemManager.insertItems([parent, child]) - await itemManager.setTagParent(parent, child) + await mutator.insertItems([parent, child]) + await mutator.setTagParent(parent, child) expect(itemManager.getTagParent(child)?.uuid).toBe(parent.uuid) }) it('findTagByTitleAndParent', async () => { - itemManager = createService() const parent = createTag('name1') const child = createTag('childName') const duplicateNameChild = createTag('name1') - await itemManager.insertItems([parent, child, duplicateNameChild]) - await itemManager.setTagParent(parent, child) - await itemManager.setTagParent(parent, duplicateNameChild) + await mutator.insertItems([parent, child, duplicateNameChild]) + await mutator.setTagParent(parent, child) + await mutator.setTagParent(parent, duplicateNameChild) const a = itemManager.findTagByTitleAndParent('name1', undefined) const b = itemManager.findTagByTitleAndParent('name1', parent) @@ -270,16 +248,16 @@ describe('itemManager', () => { it('findOrCreateTagByTitle', async () => { setupRandomUuid() - itemManager = createService() + const parent = createTag('parent') const child = createTag('child') - await itemManager.insertItems([parent, child]) - await itemManager.setTagParent(parent, child) + await mutator.insertItems([parent, child]) + await mutator.setTagParent(parent, child) - const childA = await itemManager.findOrCreateTagByTitle('child') - const childB = await itemManager.findOrCreateTagByTitle('child', parent) - const childC = await itemManager.findOrCreateTagByTitle('child-bis', parent) - const childD = await itemManager.findOrCreateTagByTitle('child-bis', parent) + const childA = await mutator.findOrCreateTagByTitle({ title: 'child' }) + const childB = await mutator.findOrCreateTagByTitle({ title: 'child', parentItemToLookupUuidFor: parent }) + const childC = await mutator.findOrCreateTagByTitle({ title: 'child-bis', parentItemToLookupUuidFor: parent }) + const childD = await mutator.findOrCreateTagByTitle({ title: 'child-bis', parentItemToLookupUuidFor: parent }) expect(childA.uuid).not.toEqual(child.uuid) expect(childB.uuid).toEqual(child.uuid) @@ -292,17 +270,16 @@ describe('itemManager', () => { }) it('findOrCreateTagParentChain', async () => { - itemManager = createService() const parent = createTag('parent') const child = createTag('child') - await itemManager.insertItems([parent, child]) - await itemManager.setTagParent(parent, child) + await mutator.insertItems([parent, child]) + await mutator.setTagParent(parent, child) - const a = await itemManager.findOrCreateTagParentChain(['parent']) - const b = await itemManager.findOrCreateTagParentChain(['parent', 'child']) - const c = await itemManager.findOrCreateTagParentChain(['parent', 'child2']) - const d = await itemManager.findOrCreateTagParentChain(['parent2', 'child1']) + const a = await mutator.findOrCreateTagParentChain(['parent']) + const b = await mutator.findOrCreateTagParentChain(['parent', 'child']) + const c = await mutator.findOrCreateTagParentChain(['parent', 'child2']) + const d = await mutator.findOrCreateTagParentChain(['parent2', 'child1']) expect(a?.uuid).toEqual(parent.uuid) expect(b?.uuid).toEqual(child.uuid) @@ -317,15 +294,14 @@ describe('itemManager', () => { }) it('isAncestor', async () => { - itemManager = createService() const grandParent = createTag('grandParent') const parent = createTag('parent') const child = createTag('child') const another = createTag('another') - await itemManager.insertItems([child, parent, grandParent, another]) - await itemManager.setTagParent(parent, child) - await itemManager.setTagParent(grandParent, parent) + await mutator.insertItems([child, parent, grandParent, another]) + await mutator.setTagParent(parent, child) + await mutator.setTagParent(grandParent, parent) expect(itemManager.isTagAncestor(grandParent, parent)).toEqual(true) expect(itemManager.isTagAncestor(grandParent, child)).toEqual(true) @@ -341,28 +317,26 @@ describe('itemManager', () => { }) it('unsetTagRelationship', async () => { - itemManager = createService() const parent = createTag('parent') const child = createTag('child') - await itemManager.insertItems([parent, child]) - await itemManager.setTagParent(parent, child) + await mutator.insertItems([parent, child]) + await mutator.setTagParent(parent, child) expect(itemManager.getTagParent(child)?.uuid).toBe(parent.uuid) - await itemManager.unsetTagParent(child) + await mutator.unsetTagParent(child) expect(itemManager.getTagParent(child)).toBeUndefined() }) it('getTagParentChain', async () => { - itemManager = createService() const greatGrandParent = createTag('greatGrandParent') const grandParent = createTag('grandParent') const parent = createTag('parent') const child = createTag('child') - await itemManager.insertItems([greatGrandParent, grandParent, parent, child]) - await itemManager.setTagParent(parent, child) - await itemManager.setTagParent(grandParent, parent) - await itemManager.setTagParent(greatGrandParent, grandParent) + await mutator.insertItems([greatGrandParent, grandParent, parent, child]) + await mutator.setTagParent(parent, child) + await mutator.setTagParent(grandParent, parent) + await mutator.setTagParent(greatGrandParent, grandParent) const uuidChain = itemManager.getTagParentChain(child).map((tag) => tag.uuid) @@ -371,18 +345,17 @@ describe('itemManager', () => { }) it('viewing notes for parent tag should not display notes of children', async () => { - itemManager = createService() const parentTag = createTag('parent') const childTag = createTag('child') - await itemManager.insertItems([parentTag, childTag]) - await itemManager.setTagParent(parentTag, childTag) + await mutator.insertItems([parentTag, childTag]) + await mutator.setTagParent(parentTag, childTag) const parentNote = createNoteWithTitle('parentNote') const childNote = createNoteWithTitle('childNote') - await itemManager.insertItems([parentNote, childNote]) + await mutator.insertItems([parentNote, childNote]) - await itemManager.addTagToNote(parentNote, parentTag, false) - await itemManager.addTagToNote(childNote, childTag, false) + await mutator.addTagToNote(parentNote, parentTag, false) + await mutator.addTagToNote(childNote, childTag, false) itemManager.setPrimaryItemDisplayOptions({ tags: [parentTag], @@ -397,7 +370,6 @@ describe('itemManager', () => { describe('template items', () => { it('create template item', async () => { - itemManager = createService() setupRandomUuid() const item = await itemManager.createTemplateItem(ContentType.Note, { @@ -412,7 +384,6 @@ describe('itemManager', () => { }) it('isTemplateItem return the correct value', async () => { - itemManager = createService() setupRandomUuid() const item = await itemManager.createTemplateItem(ContentType.Note, { @@ -422,13 +393,12 @@ describe('itemManager', () => { expect(itemManager.isTemplateItem(item)).toEqual(true) - await itemManager.insertItem(item) + await mutator.insertItem(item) expect(itemManager.isTemplateItem(item)).toEqual(false) }) it('isTemplateItem return the correct value for system smart views', () => { - itemManager = createService() setupRandomUuid() const [systemTag1, ...restOfSystemViews] = itemManager @@ -445,29 +415,27 @@ describe('itemManager', () => { describe('tags', () => { it('lets me create a regular tag with a clear API', async () => { - itemManager = createService() setupRandomUuid() - const tag = await itemManager.createTag('this is my new tag') + const tag = await mutator.createTag({ title: 'this is my new tag' }) expect(tag).toBeTruthy() expect(itemManager.isTemplateItem(tag)).toEqual(false) }) it('should search tags correctly', async () => { - itemManager = createService() setupRandomUuid() - const foo = await itemManager.createTag('foo[') - const foobar = await itemManager.createTag('foo[bar]') - const bar = await itemManager.createTag('bar[') - const barfoo = await itemManager.createTag('bar[foo]') - const fooDelimiter = await itemManager.createTag('bar.foo') - const barFooDelimiter = await itemManager.createTag('baz.bar.foo') - const fooAttached = await itemManager.createTag('Foo') + const foo = await mutator.createTag({ title: 'foo[' }) + const foobar = await mutator.createTag({ title: 'foo[bar]' }) + const bar = await mutator.createTag({ title: 'bar[' }) + const barfoo = await mutator.createTag({ title: 'bar[foo]' }) + const fooDelimiter = await mutator.createTag({ title: 'bar.foo' }) + const barFooDelimiter = await mutator.createTag({ title: 'baz.bar.foo' }) + const fooAttached = await mutator.createTag({ title: 'Foo' }) const note = createNoteWithTitle('note') - await itemManager.insertItems([foo, foobar, bar, barfoo, fooDelimiter, barFooDelimiter, fooAttached, note]) - await itemManager.addTagToNote(note, fooAttached, false) + await mutator.insertItems([foo, foobar, bar, barfoo, fooDelimiter, barFooDelimiter, fooAttached, note]) + await mutator.addTagToNote(note, fooAttached, false) const fooResults = itemManager.searchTags('foo') expect(fooResults).toContainEqual(foo) @@ -482,19 +450,17 @@ describe('itemManager', () => { describe('tags notes index', () => { it('counts countable notes', async () => { - itemManager = createService() - const parentTag = createTag('parent') const childTag = createTag('child') - await itemManager.insertItems([parentTag, childTag]) - await itemManager.setTagParent(parentTag, childTag) + await mutator.insertItems([parentTag, childTag]) + await mutator.setTagParent(parentTag, childTag) const parentNote = createNoteWithTitle('parentNote') const childNote = createNoteWithTitle('childNote') - await itemManager.insertItems([parentNote, childNote]) + await mutator.insertItems([parentNote, childNote]) - await itemManager.addTagToNote(parentNote, parentTag, false) - await itemManager.addTagToNote(childNote, childTag, false) + await mutator.addTagToNote(parentNote, parentTag, false) + await mutator.addTagToNote(childNote, childTag, false) expect(itemManager.countableNotesForTag(parentTag)).toBe(1) expect(itemManager.countableNotesForTag(childTag)).toBe(1) @@ -502,29 +468,27 @@ describe('itemManager', () => { }) it('archiving a note should update count index', async () => { - itemManager = createService() - const tag1 = createTag('tag 1') - await itemManager.insertItems([tag1]) + await mutator.insertItems([tag1]) const note1 = createNoteWithTitle('note 1') const note2 = createNoteWithTitle('note 2') - await itemManager.insertItems([note1, note2]) + await mutator.insertItems([note1, note2]) - await itemManager.addTagToNote(note1, tag1, false) - await itemManager.addTagToNote(note2, tag1, false) + await mutator.addTagToNote(note1, tag1, false) + await mutator.addTagToNote(note2, tag1, false) expect(itemManager.countableNotesForTag(tag1)).toBe(2) expect(itemManager.allCountableNotesCount()).toBe(2) - await itemManager.changeItem(note1, (m) => { + await mutator.changeItem(note1, (m) => { m.archived = true }) expect(itemManager.allCountableNotesCount()).toBe(1) expect(itemManager.countableNotesForTag(tag1)).toBe(1) - await itemManager.changeItem(note1, (m) => { + await mutator.changeItem(note1, (m) => { m.archived = false }) @@ -535,13 +499,12 @@ describe('itemManager', () => { describe('smart views', () => { it('lets me create a smart view', async () => { - itemManager = createService() setupRandomUuid() const [view1, view2, view3] = await Promise.all([ - itemManager.createSmartView('Not Pinned', NotPinnedPredicate), - itemManager.createSmartView('Last Day', LastDayPredicate), - itemManager.createSmartView('Long', LongTextPredicate), + mutator.createSmartView({ title: 'Not Pinned', predicate: NotPinnedPredicate }), + mutator.createSmartView({ title: 'Last Day', predicate: LastDayPredicate }), + mutator.createSmartView({ title: 'Long', predicate: LongTextPredicate }), ]) expect(view1).toBeTruthy() @@ -554,10 +517,9 @@ describe('itemManager', () => { }) it('lets me use a smart view', async () => { - itemManager = createService() setupRandomUuid() - const view = await itemManager.createSmartView('Not Pinned', NotPinnedPredicate) + const view = await mutator.createSmartView({ title: 'Not Pinned', predicate: NotPinnedPredicate }) const notes = itemManager.notesMatchingSmartView(view) @@ -565,7 +527,6 @@ describe('itemManager', () => { }) it('lets me test if a title is a smart view', () => { - itemManager = createService() setupRandomUuid() expect(itemManager.isSmartViewTitle(VIEW_NOT_PINNED)).toEqual(true) @@ -577,13 +538,12 @@ describe('itemManager', () => { }) it('lets me create a smart view from the DSL', async () => { - itemManager = createService() setupRandomUuid() const [tag1, tag2, tag3] = await Promise.all([ - itemManager.createSmartViewFromDSL(VIEW_NOT_PINNED), - itemManager.createSmartViewFromDSL(VIEW_LAST_DAY), - itemManager.createSmartViewFromDSL(VIEW_LONG), + mutator.createSmartViewFromDSL(VIEW_NOT_PINNED), + mutator.createSmartViewFromDSL(VIEW_LAST_DAY), + mutator.createSmartViewFromDSL(VIEW_LONG), ]) expect(tag1).toBeTruthy() @@ -596,11 +556,10 @@ describe('itemManager', () => { }) it('will create smart view or tags from the generic method', async () => { - itemManager = createService() setupRandomUuid() - const someTag = await itemManager.createTagOrSmartView('some-tag') - const someView = await itemManager.createTagOrSmartView(VIEW_LONG) + const someTag = await mutator.createTagOrSmartView('some-tag') + const someView = await mutator.createTagOrSmartView(VIEW_LONG) expect(someTag.content_type).toEqual(ContentType.Tag) expect(someView.content_type).toEqual(ContentType.SmartView) @@ -608,12 +567,11 @@ describe('itemManager', () => { }) it('lets me rename a smart view', async () => { - itemManager = createService() setupRandomUuid() - const tag = await itemManager.createSmartView('Not Pinned', NotPinnedPredicate) + const tag = await mutator.createSmartView({ title: 'Not Pinned', predicate: NotPinnedPredicate }) - await itemManager.changeItem(tag, (m) => { + await mutator.changeItem(tag, (m) => { m.title = 'New Title' }) @@ -625,10 +583,9 @@ describe('itemManager', () => { }) it('lets me find a smart view', async () => { - itemManager = createService() setupRandomUuid() - const tag = await itemManager.createSmartView('Not Pinned', NotPinnedPredicate) + const tag = await mutator.createSmartView({ title: 'Not Pinned', predicate: NotPinnedPredicate }) const view = itemManager.findItem(tag.uuid) as Models.SmartView @@ -636,7 +593,6 @@ describe('itemManager', () => { }) it('untagged notes smart view', async () => { - itemManager = createService() setupRandomUuid() const view = itemManager.untaggedNotesSmartView @@ -644,11 +600,11 @@ describe('itemManager', () => { const tag = createTag('tag') const untaggedNote = createNoteWithTitle('note') const taggedNote = createNoteWithTitle('taggedNote') - await itemManager.insertItems([tag, untaggedNote, taggedNote]) + await mutator.insertItems([tag, untaggedNote, taggedNote]) expect(itemManager.notesMatchingSmartView(view)).toHaveLength(2) - await itemManager.addTagToNote(taggedNote, tag, false) + await mutator.addTagToNote(taggedNote, tag, false) expect(itemManager.notesMatchingSmartView(view)).toHaveLength(1) @@ -657,31 +613,28 @@ describe('itemManager', () => { describe('files', () => { it('should correctly rename file to filename that has extension', async () => { - itemManager = createService() const file = createFile('initialName.ext') - await itemManager.insertItems([file]) + await mutator.insertItems([file]) - const renamedFile = await itemManager.renameFile(file, 'anotherName.anotherExt') + const renamedFile = await mutator.renameFile(file, 'anotherName.anotherExt') expect(renamedFile.name).toBe('anotherName.anotherExt') }) it('should correctly rename extensionless file to filename that has extension', async () => { - itemManager = createService() const file = createFile('initialName') - await itemManager.insertItems([file]) + await mutator.insertItems([file]) - const renamedFile = await itemManager.renameFile(file, 'anotherName.anotherExt') + const renamedFile = await mutator.renameFile(file, 'anotherName.anotherExt') expect(renamedFile.name).toBe('anotherName.anotherExt') }) it('should correctly rename file to filename that does not have extension', async () => { - itemManager = createService() const file = createFile('initialName.ext') - await itemManager.insertItems([file]) + await mutator.insertItems([file]) - const renamedFile = await itemManager.renameFile(file, 'anotherName') + const renamedFile = await mutator.renameFile(file, 'anotherName') expect(renamedFile.name).toBe('anotherName') }) @@ -689,15 +642,14 @@ describe('itemManager', () => { describe('linking', () => { it('adding a note to a tag hierarchy should add the note to its parent too', async () => { - itemManager = createService() const parentTag = createTag('parent') const childTag = createTag('child') const note = createNoteWithTitle('note') - await itemManager.insertItems([parentTag, childTag, note]) - await itemManager.setTagParent(parentTag, childTag) + await mutator.insertItems([parentTag, childTag, note]) + await mutator.setTagParent(parentTag, childTag) - await itemManager.addTagToNote(note, childTag, true) + await mutator.addTagToNote(note, childTag, true) const tags = itemManager.getSortedTagsForItem(note) @@ -707,15 +659,14 @@ describe('itemManager', () => { }) it('adding a note to a tag hierarchy should not add the note to its parent if hierarchy option is disabled', async () => { - itemManager = createService() const parentTag = createTag('parent') const childTag = createTag('child') const note = createNoteWithTitle('note') - await itemManager.insertItems([parentTag, childTag, note]) - await itemManager.setTagParent(parentTag, childTag) + await mutator.insertItems([parentTag, childTag, note]) + await mutator.setTagParent(parentTag, childTag) - await itemManager.addTagToNote(note, childTag, false) + await mutator.addTagToNote(note, childTag, false) const tags = itemManager.getSortedTagsForItem(note) @@ -724,15 +675,14 @@ describe('itemManager', () => { }) it('adding a file to a tag hierarchy should add the file to its parent too', async () => { - itemManager = createService() const parentTag = createTag('parent') const childTag = createTag('child') const file = createFile('file') - await itemManager.insertItems([parentTag, childTag, file]) - await itemManager.setTagParent(parentTag, childTag) + await mutator.insertItems([parentTag, childTag, file]) + await mutator.setTagParent(parentTag, childTag) - await itemManager.addTagToFile(file, childTag, true) + await mutator.addTagToFile(file, childTag, true) const tags = itemManager.getSortedTagsForItem(file) @@ -742,15 +692,14 @@ describe('itemManager', () => { }) it('adding a file to a tag hierarchy should not add the file to its parent if hierarchy option is disabled', async () => { - itemManager = createService() const parentTag = createTag('parent') const childTag = createTag('child') const file = createFile('file') - await itemManager.insertItems([parentTag, childTag, file]) - await itemManager.setTagParent(parentTag, childTag) + await mutator.insertItems([parentTag, childTag, file]) + await mutator.setTagParent(parentTag, childTag) - await itemManager.addTagToFile(file, childTag, false) + await mutator.addTagToFile(file, childTag, false) const tags = itemManager.getSortedTagsForItem(file) @@ -759,12 +708,12 @@ describe('itemManager', () => { }) it('should link file with note', async () => { - itemManager = createService() const note = createNoteWithTitle('invoices') const file = createFile('invoice_1.pdf') - await itemManager.insertItems([note, file]) + await mutator.insertItems([note, file]) - const resultingFile = await itemManager.associateFileWithNote(file, note) + const resultingFile = await mutator.associateFileWithNote(file, note) + assert(resultingFile) const references = resultingFile.references expect(references).toHaveLength(1) @@ -772,25 +721,24 @@ describe('itemManager', () => { }) it('should unlink file from note', async () => { - itemManager = createService() const note = createNoteWithTitle('invoices') const file = createFile('invoice_1.pdf') - await itemManager.insertItems([note, file]) + await mutator.insertItems([note, file]) - const associatedFile = await itemManager.associateFileWithNote(file, note) - const disassociatedFile = await itemManager.disassociateFileWithNote(associatedFile, note) + const associatedFile = await mutator.associateFileWithNote(file, note) + assert(associatedFile) + const disassociatedFile = await mutator.disassociateFileWithNote(associatedFile, note) const references = disassociatedFile.references expect(references).toHaveLength(0) }) it('should link note to note', async () => { - itemManager = createService() const note = createNoteWithTitle('research') const note2 = createNoteWithTitle('citation') - await itemManager.insertItems([note, note2]) + await mutator.insertItems([note, note2]) - const resultingNote = await itemManager.linkNoteToNote(note, note2) + const resultingNote = await mutator.linkNoteToNote(note, note2) const references = resultingNote.references expect(references).toHaveLength(1) @@ -798,12 +746,11 @@ describe('itemManager', () => { }) it('should link file to file', async () => { - itemManager = createService() const file = createFile('research') const file2 = createFile('citation') - await itemManager.insertItems([file, file2]) + await mutator.insertItems([file, file2]) - const resultingfile = await itemManager.linkFileToFile(file, file2) + const resultingfile = await mutator.linkFileToFile(file, file2) const references = resultingfile.references expect(references).toHaveLength(1) @@ -811,13 +758,12 @@ describe('itemManager', () => { }) it('should get the relationship type for two items', async () => { - itemManager = createService() const firstNote = createNoteWithTitle('First note') const secondNote = createNoteWithTitle('Second note') const unlinkedNote = createNoteWithTitle('Unlinked note') - await itemManager.insertItems([firstNote, secondNote, unlinkedNote]) + await mutator.insertItems([firstNote, secondNote, unlinkedNote]) - const firstNoteLinkedToSecond = await itemManager.linkNoteToNote(firstNote, secondNote) + const firstNoteLinkedToSecond = await mutator.linkNoteToNote(firstNote, secondNote) const relationshipOfFirstNoteToSecond = itemManager.relationshipDirectionBetweenItems( firstNoteLinkedToSecond, @@ -838,13 +784,12 @@ describe('itemManager', () => { }) it('should unlink itemOne from itemTwo if relation is direct', async () => { - itemManager = createService() const note = createNoteWithTitle('Note 1') const note2 = createNoteWithTitle('Note 2') - await itemManager.insertItems([note, note2]) + await mutator.insertItems([note, note2]) - const linkedItem = await itemManager.linkNoteToNote(note, note2) - const unlinkedItem = await itemManager.unlinkItems(linkedItem, note2) + const linkedItem = await mutator.linkNoteToNote(note, note2) + const unlinkedItem = await mutator.unlinkItems(linkedItem, note2) const references = unlinkedItem.references expect(unlinkedItem.uuid).toBe(note.uuid) @@ -852,13 +797,12 @@ describe('itemManager', () => { }) it('should unlink itemTwo from itemOne if relation is indirect', async () => { - itemManager = createService() const note = createNoteWithTitle('Note 1') const note2 = createNoteWithTitle('Note 2') - await itemManager.insertItems([note, note2]) + await mutator.insertItems([note, note2]) - const linkedItem = await itemManager.linkNoteToNote(note, note2) - const changedItem = await itemManager.unlinkItems(linkedItem, note2) + const linkedItem = await mutator.linkNoteToNote(note, note2) + const changedItem = await mutator.unlinkItems(linkedItem, note2) expect(changedItem.uuid).toBe(note.uuid) expect(changedItem.references).toHaveLength(0) diff --git a/packages/snjs/lib/Services/Items/ItemManager.ts b/packages/snjs/lib/Services/Items/ItemManager.ts index ae0188778..ad541c063 100644 --- a/packages/snjs/lib/Services/Items/ItemManager.ts +++ b/packages/snjs/lib/Services/Items/ItemManager.ts @@ -1,14 +1,13 @@ import { ContentType } from '@standardnotes/common' import { assert, naturalSort, removeFromArray, UuidGenerator, Uuids } from '@standardnotes/utils' -import { ItemsKeyMutator, SNItemsKey } from '@standardnotes/encryption' +import { SNItemsKey } from '@standardnotes/encryption' import { PayloadManager } from '../Payloads/PayloadManager' import { TagsToFoldersMigrationApplicator } from '../../Migrations/Applicators/TagsToFolders' import { UuidString } from '../../Types/UuidString' import * as Models from '@standardnotes/models' import * as Services from '@standardnotes/services' import { PayloadManagerChangeData } from '../Payloads' -import { DiagnosticInfo, ItemsClientInterface, ItemRelationshipDirection } from '@standardnotes/services' -import { CollectionSort, DecryptedItemInterface, ItemContent, SmartViewDefaultIconName } from '@standardnotes/models' +import { ItemRelationshipDirection } from '@standardnotes/services' type ItemsChangeObserver = { contentType: ContentType[] @@ -23,18 +22,18 @@ type ItemsChangeObserver void private observers: ItemsChangeObserver[] = [] private collection!: Models.ItemCollection private systemSmartViews: Models.SmartView[] - private tagItemsIndex!: Models.TagItemsIndex + private itemCounter!: Models.ItemCounter - private navigationDisplayController!: Models.ItemDisplayController - private tagDisplayController!: Models.ItemDisplayController + private navigationDisplayController!: Models.ItemDisplayController< + Models.SNNote | Models.FileItem, + Models.NotesAndFilesDisplayOptions + > + private tagDisplayController!: Models.ItemDisplayController private itemsKeyDisplayController!: Models.ItemDisplayController private componentDisplayController!: Models.ItemDisplayController private themeDisplayController!: Models.ItemDisplayController @@ -52,11 +51,15 @@ export class ItemManager this.unsubChangeObserver = this.payloadManager.addObserver(ContentType.Any, this.setPayloads.bind(this)) } - private rebuildSystemSmartViews(criteria: Models.FilterDisplayOptions): Models.SmartView[] { + private rebuildSystemSmartViews(criteria: Models.NotesAndFilesDisplayOptions): Models.SmartView[] { this.systemSmartViews = Models.BuildSmartViews(criteria) return this.systemSmartViews } + public getCollection(): Models.ItemCollection { + return this.collection + } + private createCollection() { this.collection = new Models.ItemCollection() @@ -94,7 +97,7 @@ export class ItemManager sortDirection: 'asc', }) - this.tagItemsIndex = new Models.TagItemsIndex(this.collection, this.tagItemsIndex?.observers) + this.itemCounter = new Models.ItemCounter(this.collection, this.itemCounter?.observers) } private get allDisplayControllers(): Models.ItemDisplayController[] { @@ -113,6 +116,10 @@ export class ItemManager return this.collection.invalidElements() } + public get invalidNonVaultedItems(): Models.EncryptedItemInterface[] { + return this.invalidItems.filter((item) => !item.key_system_identifier) + } + public createItemFromPayload(payload: Models.DecryptedPayloadInterface): Models.DecryptedItemInterface { return Models.CreateDecryptedItemFromPayload(payload) } @@ -121,8 +128,8 @@ export class ItemManager return new Models.DecryptedPayload(object) } - public setPrimaryItemDisplayOptions(options: Models.DisplayOptions): void { - const override: Models.FilterDisplayOptions = {} + public setPrimaryItemDisplayOptions(options: Models.NotesAndFilesDisplayControllerOptions): void { + const override: Models.NotesAndFilesDisplayOptions = {} const additionalFilters: Models.ItemFilter[] = [] if (options.views && options.views.find((view) => view.uuid === Models.SystemViewId.AllNotes)) { @@ -164,7 +171,7 @@ export class ItemManager }) .filter((view) => view != undefined) - const updatedOptions: Models.DisplayOptions = { + const updatedOptions: Models.DisplayControllerDisplayOptions & Models.NotesAndFilesDisplayOptions = { ...options, ...override, ...{ @@ -173,7 +180,7 @@ export class ItemManager }, } - if (updatedOptions.sortBy === CollectionSort.Title) { + if (updatedOptions.sortBy === Models.CollectionSort.Title) { updatedOptions.sortDirection = updatedOptions.sortDirection === 'asc' ? 'dsc' : 'asc' } @@ -181,6 +188,17 @@ export class ItemManager customFilter: Models.computeUnifiedFilterForDisplayOptions(updatedOptions, this.collection, additionalFilters), ...updatedOptions, }) + + this.itemCounter.setDisplayOptions(updatedOptions) + } + + public setVaultDisplayOptions(options: Models.VaultDisplayOptions): void { + this.navigationDisplayController.setVaultDisplayOptions(options) + this.tagDisplayController.setVaultDisplayOptions(options) + this.smartViewDisplayController.setVaultDisplayOptions(options) + this.fileDisplayController.setVaultDisplayOptions(options) + + this.itemCounter.setVaultDisplayOptions(options) } public getDisplayableNotes(): Models.SNNote[] { @@ -214,7 +232,7 @@ export class ItemManager ;(this.unsubChangeObserver as unknown) = undefined ;(this.payloadManager as unknown) = undefined ;(this.collection as unknown) = undefined - ;(this.tagItemsIndex as unknown) = undefined + ;(this.itemCounter as unknown) = undefined ;(this.tagDisplayController as unknown) = undefined ;(this.navigationDisplayController as unknown) = undefined ;(this.itemsKeyDisplayController as unknown) = undefined @@ -252,9 +270,6 @@ export class ItemManager return this.findItem(uuid) as T } - /** - * Returns all items matching given ids - */ findItems(uuids: UuidString[]): T[] { return this.collection.findAllDecrypted(uuids) as T[] } @@ -271,6 +286,7 @@ export class ItemManager return this.collection.nondeletedElements().filter(Models.isDecryptedItem) } + /** Unlock .items, this function includes error decrypting items */ allTrackedItems(): Models.ItemInterface[] { return this.collection.all() } @@ -280,26 +296,26 @@ export class ItemManager } public addNoteCountChangeObserver(observer: Models.TagItemCountChangeObserver): () => void { - return this.tagItemsIndex.addCountChangeObserver(observer) + return this.itemCounter.addCountChangeObserver(observer) } public allCountableNotesCount(): number { - return this.tagItemsIndex.allCountableNotesCount() + return this.itemCounter.allCountableNotesCount() } public allCountableFilesCount(): number { - return this.tagItemsIndex.allCountableFilesCount() + return this.itemCounter.allCountableFilesCount() } public countableNotesForTag(tag: Models.SNTag | Models.SmartView): number { if (tag instanceof Models.SmartView) { if (tag.uuid === Models.SystemViewId.AllNotes) { - return this.tagItemsIndex.allCountableNotesCount() + return this.itemCounter.allCountableNotesCount() } throw Error('countableItemsForTag is not meant to be used for smart views.') } - return this.tagItemsIndex.countableItemsForTag(tag) + return this.itemCounter.countableItemsForTag(tag) } public getNoteCount(): number { @@ -330,12 +346,12 @@ export class ItemManager /** * Returns the items that reference the given item, or an empty array if no results. */ - public itemsReferencingItem( - itemToLookupUuidFor: Models.DecryptedItemInterface, + public itemsReferencingItem( + itemToLookupUuidFor: { uuid: UuidString }, contentType?: ContentType, - ): Models.DecryptedItemInterface[] { + ): I[] { const uuids = this.collection.uuidsThatReferenceUuid(itemToLookupUuidFor.uuid) - let referencing = this.findItems(uuids) + let referencing = this.findItems(uuids) if (contentType) { referencing = referencing.filter((ref) => { return ref?.content_type === contentType @@ -405,7 +421,7 @@ export class ItemManager } this.collection.onChange(delta) - this.tagItemsIndex.onChange(delta) + this.itemCounter.onChange(delta) const affectedContentTypesArray = Array.from(affectedContentTypes.values()) for (const controller of this.allDisplayControllers) { @@ -509,250 +525,6 @@ export class ItemManager } } - /** - * Consumers wanting to modify an item should run it through this block, - * so that data is properly mapped through our function, and latest state - * is properly reconciled. - */ - public async changeItem< - M extends Models.DecryptedItemMutator = Models.DecryptedItemMutator, - I extends Models.DecryptedItemInterface = Models.DecryptedItemInterface, - >( - itemToLookupUuidFor: I, - mutate?: (mutator: M) => void, - mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps, - emitSource = Models.PayloadEmitSource.LocalChanged, - payloadSourceKey?: string, - ): Promise { - const results = await this.changeItems( - [itemToLookupUuidFor], - mutate, - mutationType, - emitSource, - payloadSourceKey, - ) - return results[0] - } - - /** - * @param mutate If not supplied, the intention would simply be to mark the item as dirty. - */ - public async changeItems< - M extends Models.DecryptedItemMutator = Models.DecryptedItemMutator, - I extends Models.DecryptedItemInterface = Models.DecryptedItemInterface, - >( - itemsToLookupUuidsFor: I[], - mutate?: (mutator: M) => void, - mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps, - emitSource = Models.PayloadEmitSource.LocalChanged, - payloadSourceKey?: string, - ): Promise { - const items = this.findItemsIncludingBlanks(Uuids(itemsToLookupUuidsFor)) - const payloads: Models.DecryptedPayloadInterface[] = [] - - for (const item of items) { - if (!item) { - throw Error('Attempting to change non-existant item') - } - const mutator = Models.CreateDecryptedMutatorForItem(item, mutationType) - if (mutate) { - mutate(mutator as M) - } - const payload = mutator.getResult() - payloads.push(payload) - } - - await this.payloadManager.emitPayloads(payloads, emitSource, payloadSourceKey) - - const results = this.findItems(payloads.map((p) => p.uuid)) as I[] - - return results - } - - /** - * Run unique mutations per each item in the array, then only propagate all changes - * once all mutations have been run. This differs from `changeItems` in that changeItems - * runs the same mutation on all items. - */ - public async runTransactionalMutations( - transactions: Models.TransactionalMutation[], - emitSource = Models.PayloadEmitSource.LocalChanged, - payloadSourceKey?: string, - ): Promise<(Models.DecryptedItemInterface | undefined)[]> { - const payloads: Models.DecryptedPayloadInterface[] = [] - - for (const transaction of transactions) { - const item = this.findItem(transaction.itemUuid) - - if (!item) { - continue - } - - const mutator = Models.CreateDecryptedMutatorForItem( - item, - transaction.mutationType || Models.MutationType.UpdateUserTimestamps, - ) - - transaction.mutate(mutator) - const payload = mutator.getResult() - payloads.push(payload) - } - - await this.payloadManager.emitPayloads(payloads, emitSource, payloadSourceKey) - const results = this.findItems(payloads.map((p) => p.uuid)) - return results - } - - public async runTransactionalMutation( - transaction: Models.TransactionalMutation, - emitSource = Models.PayloadEmitSource.LocalChanged, - payloadSourceKey?: string, - ): Promise { - const item = this.findSureItem(transaction.itemUuid) - const mutator = Models.CreateDecryptedMutatorForItem( - item, - transaction.mutationType || Models.MutationType.UpdateUserTimestamps, - ) - transaction.mutate(mutator) - const payload = mutator.getResult() - - await this.payloadManager.emitPayloads([payload], emitSource, payloadSourceKey) - const result = this.findItem(payload.uuid) - return result - } - - async changeNote( - itemToLookupUuidFor: Models.SNNote, - mutate: (mutator: Models.NoteMutator) => void, - mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps, - emitSource = Models.PayloadEmitSource.LocalChanged, - payloadSourceKey?: string, - ): Promise { - const note = this.findItem(itemToLookupUuidFor.uuid) - if (!note) { - throw Error('Attempting to change non-existant note') - } - const mutator = new Models.NoteMutator(note, mutationType) - - return this.applyTransform(mutator, mutate, emitSource, payloadSourceKey) - } - - async changeTag( - itemToLookupUuidFor: Models.SNTag, - mutate: (mutator: Models.TagMutator) => void, - mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps, - emitSource = Models.PayloadEmitSource.LocalChanged, - payloadSourceKey?: string, - ): Promise { - const tag = this.findItem(itemToLookupUuidFor.uuid) - if (!tag) { - throw Error('Attempting to change non-existant tag') - } - const mutator = new Models.TagMutator(tag, mutationType) - await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey) - return this.findSureItem(itemToLookupUuidFor.uuid) - } - - async changeComponent( - itemToLookupUuidFor: Models.SNComponent, - mutate: (mutator: Models.ComponentMutator) => void, - mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps, - emitSource = Models.PayloadEmitSource.LocalChanged, - payloadSourceKey?: string, - ): Promise { - const component = this.findItem(itemToLookupUuidFor.uuid) - if (!component) { - throw Error('Attempting to change non-existant component') - } - const mutator = new Models.ComponentMutator(component, mutationType) - await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey) - return this.findSureItem(itemToLookupUuidFor.uuid) - } - - async changeFeatureRepo( - itemToLookupUuidFor: Models.SNFeatureRepo, - mutate: (mutator: Models.FeatureRepoMutator) => void, - mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps, - emitSource = Models.PayloadEmitSource.LocalChanged, - payloadSourceKey?: string, - ): Promise { - const repo = this.findItem(itemToLookupUuidFor.uuid) - if (!repo) { - throw Error('Attempting to change non-existant repo') - } - const mutator = new Models.FeatureRepoMutator(repo, mutationType) - await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey) - return this.findSureItem(itemToLookupUuidFor.uuid) - } - - async changeActionsExtension( - itemToLookupUuidFor: Models.SNActionsExtension, - mutate: (mutator: Models.ActionsExtensionMutator) => void, - mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps, - emitSource = Models.PayloadEmitSource.LocalChanged, - payloadSourceKey?: string, - ): Promise { - const extension = this.findItem(itemToLookupUuidFor.uuid) - if (!extension) { - throw Error('Attempting to change non-existant extension') - } - const mutator = new Models.ActionsExtensionMutator(extension, mutationType) - await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey) - return this.findSureItem(itemToLookupUuidFor.uuid) - } - - async changeItemsKey( - itemToLookupUuidFor: Models.ItemsKeyInterface, - mutate: (mutator: Models.ItemsKeyMutatorInterface) => void, - mutationType: Models.MutationType = Models.MutationType.UpdateUserTimestamps, - emitSource = Models.PayloadEmitSource.LocalChanged, - payloadSourceKey?: string, - ): Promise { - const itemsKey = this.findItem(itemToLookupUuidFor.uuid) - - if (!itemsKey) { - throw Error('Attempting to change non-existant itemsKey') - } - - const mutator = new ItemsKeyMutator(itemsKey, mutationType) - - await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey) - - return this.findSureItem(itemToLookupUuidFor.uuid) - } - - private async applyTransform( - mutator: T, - mutate: (mutator: T) => void, - emitSource = Models.PayloadEmitSource.LocalChanged, - payloadSourceKey?: string, - ): Promise { - mutate(mutator) - const payload = mutator.getResult() - return this.payloadManager.emitPayload(payload, emitSource, payloadSourceKey) - } - - /** - * Sets the item as needing sync. The item is then run through the mapping function, - * and propagated to mapping observers. - * @param isUserModified - Whether to update the item's "user modified date" - */ - public async setItemDirty(itemToLookupUuidFor: Models.DecryptedItemInterface, isUserModified = false) { - const result = await this.setItemsDirty([itemToLookupUuidFor], isUserModified) - return result[0] - } - - public async setItemsDirty( - itemsToLookupUuidsFor: Models.DecryptedItemInterface[], - isUserModified = false, - ): Promise { - return this.changeItems( - itemsToLookupUuidsFor, - undefined, - isUserModified ? Models.MutationType.UpdateUserTimestamps : Models.MutationType.NoUpdateUserTimestamps, - ) - } - /** * Returns an array of items that need to be synced. */ @@ -760,47 +532,6 @@ export class ItemManager return this.collection.dirtyElements().filter(Models.isDecryptedOrDeletedItem) } - /** - * Duplicates an item and maps it, thus propagating the item to observers. - * @param isConflict - Whether to mark the duplicate as a conflict of the original. - */ - public async duplicateItem( - itemToLookupUuidFor: T, - isConflict = false, - additionalContent?: Partial, - ) { - const item = this.findSureItem(itemToLookupUuidFor.uuid) - const payload = item.payload.copy() - const resultingPayloads = Models.PayloadsByDuplicating({ - payload, - baseCollection: this.payloadManager.getMasterCollection(), - isConflict, - additionalContent, - }) - - await this.payloadManager.emitPayloads(resultingPayloads, Models.PayloadEmitSource.LocalChanged) - const duplicate = this.findSureItem(resultingPayloads[0].uuid) - return duplicate - } - - public async createItem( - contentType: ContentType, - content: C, - needsSync = false, - ): Promise { - const payload = new Models.DecryptedPayload({ - uuid: UuidGenerator.GenerateUuid(), - content_type: contentType, - content: Models.FillItemContent(content), - dirty: needsSync, - ...Models.PayloadTimestampDefaults(), - }) - - await this.payloadManager.emitPayload(payload, Models.PayloadEmitSource.LocalInserted) - - return this.findSureItem(payload.uuid) - } - public createTemplateItem< C extends Models.ItemContent = Models.ItemContent, I extends Models.DecryptedItemInterface = Models.DecryptedItemInterface, @@ -824,75 +555,6 @@ export class ItemManager return !this.findItem(item.uuid) } - public async insertItem(item: Models.DecryptedItemInterface): Promise { - return this.emitItemFromPayload(item.payload, Models.PayloadEmitSource.LocalChanged) - } - - public async insertItems( - items: Models.DecryptedItemInterface[], - emitSource: Models.PayloadEmitSource = Models.PayloadEmitSource.LocalInserted, - ): Promise { - return this.emitItemsFromPayloads( - items.map((item) => item.payload), - emitSource, - ) - } - - public async emitItemFromPayload( - payload: Models.DecryptedPayloadInterface, - emitSource: Models.PayloadEmitSource, - ): Promise { - await this.payloadManager.emitPayload(payload, emitSource) - - return this.findSureItem(payload.uuid) - } - - public async emitItemsFromPayloads( - payloads: Models.DecryptedPayloadInterface[], - emitSource: Models.PayloadEmitSource, - ): Promise { - await this.payloadManager.emitPayloads(payloads, emitSource) - - const uuids = Uuids(payloads) - - return this.findItems(uuids) - } - - public async setItemToBeDeleted( - itemToLookupUuidFor: Models.DecryptedItemInterface | Models.EncryptedItemInterface, - source: Models.PayloadEmitSource = Models.PayloadEmitSource.LocalChanged, - ): Promise { - const referencingIdsCapturedBeforeChanges = this.collection.uuidsThatReferenceUuid(itemToLookupUuidFor.uuid) - - const item = this.findAnyItem(itemToLookupUuidFor.uuid) - - if (!item) { - return - } - - const mutator = new Models.DeleteItemMutator(item, Models.MutationType.UpdateUserTimestamps) - - const deletedPayload = mutator.getDeletedResult() - - await this.payloadManager.emitPayload(deletedPayload, source) - - for (const referencingId of referencingIdsCapturedBeforeChanges) { - const referencingItem = this.findItem(referencingId) - - if (referencingItem) { - await this.changeItem(referencingItem, (mutator) => { - mutator.removeItemAsRelationship(item) - }) - } - } - } - - public async setItemsToBeDeleted( - itemsToLookupUuidsFor: (Models.DecryptedItemInterface | Models.EncryptedItemInterface)[], - ): Promise { - await Promise.all(itemsToLookupUuidsFor.map((item) => this.setItemToBeDeleted(item))) - } - public getItems(contentType: ContentType | ContentType[]): T[] { return this.collection.allDecrypted(contentType) } @@ -1018,20 +680,6 @@ export class ItemManager return chain } - public async findOrCreateTagParentChain(titlesHierarchy: string[]): Promise { - let current: Models.SNTag | undefined = undefined - - for (const title of titlesHierarchy) { - current = await this.findOrCreateTagByTitle(title, current) - } - - if (!current) { - throw new Error('Invalid tag hierarchy') - } - - return current - } - public getTagChildren(itemToLookupUuidFor: Models.SNTag): Models.SNTag[] { const tag = this.findItem(itemToLookupUuidFor.uuid) if (!tag) { @@ -1079,117 +727,12 @@ export class ItemManager return true } - /** - * @returns The changed child tag - */ - public setTagParent(parentTag: Models.SNTag, childTag: Models.SNTag): Promise { - if (parentTag.uuid === childTag.uuid) { - throw new Error('Can not set a tag parent of itself') - } - - if (this.isTagAncestor(childTag, parentTag)) { - throw new Error('Can not set a tag ancestor of itself') - } - - return this.changeTag(childTag, (m) => { - m.makeChildOf(parentTag) - }) - } - - /** - * @returns The changed child tag - */ - public unsetTagParent(childTag: Models.SNTag): Promise { - const parentTag = this.getTagParent(childTag) - - if (!parentTag) { - return Promise.resolve(childTag) - } - - return this.changeTag(childTag, (m) => { - m.unsetParent() - }) - } - - public async associateFileWithNote(file: Models.FileItem, note: Models.SNNote): Promise { - return this.changeItem(file, (mutator) => { - mutator.addNote(note) - }) - } - - public async disassociateFileWithNote(file: Models.FileItem, note: Models.SNNote): Promise { - return this.changeItem(file, (mutator) => { - mutator.removeNote(note) - }) - } - - public async addTagToNote(note: Models.SNNote, tag: Models.SNTag, addHierarchy: boolean): Promise { - let tagsToAdd = [tag] - - if (addHierarchy) { - const parentChainTags = this.getTagParentChain(tag) - tagsToAdd = [...parentChainTags, tag] - } - - return Promise.all( - tagsToAdd.map((tagToAdd) => { - return this.changeTag(tagToAdd, (mutator) => { - mutator.addNote(note) - }) as Promise - }), - ) - } - - public async addTagToFile(file: Models.FileItem, tag: Models.SNTag, addHierarchy: boolean): Promise { - let tagsToAdd = [tag] - - if (addHierarchy) { - const parentChainTags = this.getTagParentChain(tag) - tagsToAdd = [...parentChainTags, tag] - } - - return Promise.all( - tagsToAdd.map((tagToAdd) => { - return this.changeTag(tagToAdd, (mutator) => { - mutator.addFile(file) - }) as Promise - }), - ) - } - - public async linkNoteToNote(note: Models.SNNote, otherNote: Models.SNNote): Promise { - return this.changeItem(note, (mutator) => { - mutator.addNote(otherNote) - }) - } - - public async linkFileToFile(file: Models.FileItem, otherFile: Models.FileItem): Promise { - return this.changeItem(file, (mutator) => { - mutator.addFile(otherFile) - }) - } - - public async unlinkItems(itemA: DecryptedItemInterface, itemB: DecryptedItemInterface) { - const relationshipDirection = this.relationshipDirectionBetweenItems(itemA, itemB) - - if (relationshipDirection === ItemRelationshipDirection.NoRelationship) { - throw new Error('Trying to unlink already unlinked items') - } - - const itemToChange = relationshipDirection === ItemRelationshipDirection.AReferencesB ? itemA : itemB - const itemToRemove = itemToChange === itemA ? itemB : itemA - - return this.changeItem(itemToChange, (mutator) => { - mutator.removeItemAsRelationship(itemToRemove) - }) - } - /** * Get tags for a note sorted in natural order * @param item - The item whose tags will be returned * @returns Array containing tags associated with an item */ - public getSortedTagsForItem(item: DecryptedItemInterface): Models.SNTag[] { + public getSortedTagsForItem(item: Models.DecryptedItemInterface): Models.SNTag[] { return naturalSort( this.itemsReferencingItem(item).filter((ref) => { return ref?.content_type === ContentType.Tag @@ -1198,81 +741,16 @@ export class ItemManager ) } - public async createTag(title: string, parentItemToLookupUuidFor?: Models.SNTag): Promise { - const newTag = await this.createItem( - ContentType.Tag, - Models.FillItemContent({ title }), - true, - ) - - if (parentItemToLookupUuidFor) { - const parentTag = this.findItem(parentItemToLookupUuidFor.uuid) - if (!parentTag) { - throw new Error('Invalid parent tag') - } - return this.changeTag(newTag, (m) => { - m.makeChildOf(parentTag) - }) - } - - return newTag - } - - public async createSmartView( - title: string, - predicate: Models.PredicateInterface, - iconString?: string, - ): Promise { - return this.createItem( - ContentType.SmartView, - Models.FillItemContent({ - title, - predicate: predicate.toJson(), - iconString: iconString || SmartViewDefaultIconName, - } as Models.SmartViewContent), - true, - ) as Promise - } - - public async createSmartViewFromDSL(dsl: string): Promise { - let components = null - try { - components = JSON.parse(dsl.substring(1, dsl.length)) - } catch (e) { - throw Error('Invalid smart view syntax') - } - - const title = components[0] - const predicate = Models.predicateFromDSLString(dsl) - return this.createSmartView(title, predicate) - } - - public async createTagOrSmartView(title: string): Promise { - if (this.isSmartViewTitle(title)) { - return this.createSmartViewFromDSL(title) - } else { - return this.createTag(title) - } - } - public isSmartViewTitle(title: string): boolean { return title.startsWith(Models.SMART_TAG_DSL_PREFIX) } - /** - * Finds or creates a tag with a given title - */ - public async findOrCreateTagByTitle(title: string, parentItemToLookupUuidFor?: Models.SNTag): Promise { - const tag = this.findTagByTitleAndParent(title, parentItemToLookupUuidFor) - return tag || this.createTag(title, parentItemToLookupUuidFor) - } - public notesMatchingSmartView(view: Models.SmartView): Models.SNNote[] { - const criteria: Models.FilterDisplayOptions = { + const criteria: Models.NotesAndFilesDisplayOptions = { views: [view], } - return Models.itemsMatchingOptions( + return Models.notesAndFilesMatchingOptions( criteria, this.collection.allDecrypted(ContentType.Note), this.collection, @@ -1299,14 +777,6 @@ export class ItemManager return this.notesMatchingSmartView(this.trashSmartView) } - /** - * Permanently deletes any items currently in the trash. Consumer must manually call sync. - */ - public async emptyTrash(): Promise { - const notes = this.trashedItems - await this.setItemsToBeDeleted(notes) - } - /** * Returns all smart views, sorted by title. */ @@ -1346,53 +816,29 @@ export class ItemManager this.payloadManager.resetState() } - public removeItemLocally(item: Models.DecryptedItemInterface | Models.DeletedItemInterface): void { - this.collection.discard([item]) - this.payloadManager.removePayloadLocally(item.payload) + /** + * Important: Caller must coordinate with storage service separately to delete item from persistent database. + */ + public removeItemLocally(item: Models.AnyItemInterface): void { + this.removeItemsLocally([item]) + } - const delta = Models.CreateItemDelta({ discarded: [item] as Models.DeletedItemInterface[] }) + /** + * Important: Caller must coordinate with storage service separately to delete item from persistent database. + */ + public removeItemsLocally(items: Models.AnyItemInterface[]): void { + this.collection.discard(items) + this.payloadManager.removePayloadLocally(items.map((item) => item.payload)) + + const delta = Models.CreateItemDelta({ discarded: items as Models.DeletedItemInterface[] }) + const affectedContentTypes = items.map((item) => item.content_type) for (const controller of this.allDisplayControllers) { - if (controller.contentTypes.some((ct) => ct === item.content_type)) { + if (controller.contentTypes.some((ct) => affectedContentTypes.includes(ct))) { controller.onCollectionChange(delta) } } } - public renameFile(file: Models.FileItem, name: string): Promise { - return this.changeItem(file, (mutator) => { - mutator.name = name - }) - } - - public async setLastSyncBeganForItems( - itemsToLookupUuidsFor: (Models.DecryptedItemInterface | Models.DeletedItemInterface)[], - date: Date, - globalDirtyIndex: number, - ): Promise<(Models.DecryptedItemInterface | Models.DeletedItemInterface)[]> { - const uuids = Uuids(itemsToLookupUuidsFor) - - const items = this.collection.findAll(uuids).filter(Models.isDecryptedOrDeletedItem) - - const payloads: (Models.DecryptedPayloadInterface | Models.DeletedPayloadInterface)[] = [] - - for (const item of items) { - const mutator = new Models.ItemMutator( - item, - Models.MutationType.NonDirtying, - ) - - mutator.setBeginSync(date, globalDirtyIndex) - - const payload = mutator.getResult() - - payloads.push(payload) - } - - await this.payloadManager.emitPayloads(payloads, Models.PayloadEmitSource.PreSyncSave) - - return this.findAnyItems(uuids) as (Models.DecryptedItemInterface | Models.DeletedItemInterface)[] - } - public relationshipDirectionBetweenItems( itemA: Models.DecryptedItemInterface, itemB: Models.DecryptedItemInterface, @@ -1407,12 +853,8 @@ export class ItemManager : ItemRelationshipDirection.NoRelationship } - override getDiagnostics(): Promise { - return Promise.resolve({ - items: { - allIds: Uuids(this.collection.all()), - }, - }) + itemsBelongingToKeySystem(systemIdentifier: Models.KeySystemIdentifier): Models.DecryptedItemInterface[] { + return this.items.filter((item) => item.key_system_identifier === systemIdentifier) } public conflictsOf(uuid: string) { @@ -1422,4 +864,8 @@ export class ItemManager public numberOfNotesWithConflicts(): number { return this.findItems(this.collection.uuidsOfItemsWithConflicts()).filter(Models.isNote).length } + + getNoteLinkedFiles(note: Models.SNNote): Models.FileItem[] { + return this.itemsReferencingItem(note).filter(Models.isFile) + } } diff --git a/packages/snjs/lib/Services/KeyRecovery/KeyRecoveryService.ts b/packages/snjs/lib/Services/KeyRecovery/KeyRecoveryService.ts index 670471ebe..2cb3c5f9d 100644 --- a/packages/snjs/lib/Services/KeyRecovery/KeyRecoveryService.ts +++ b/packages/snjs/lib/Services/KeyRecovery/KeyRecoveryService.ts @@ -312,7 +312,7 @@ export class SNKeyRecoveryService extends AbstractService(note, (mutator) => { + await this.mutator.changeItem(note, (mutator) => { mutator.authorizedForListed = true }) + void this.sync.sync() + return true } diff --git a/packages/snjs/lib/Services/Mutator/MutatorService.spec.ts b/packages/snjs/lib/Services/Mutator/MutatorService.spec.ts index 693ad20a8..f0804c8da 100644 --- a/packages/snjs/lib/Services/Mutator/MutatorService.spec.ts +++ b/packages/snjs/lib/Services/Mutator/MutatorService.spec.ts @@ -1,16 +1,16 @@ -import { SNHistoryManager } from './../History/HistoryManager' -import { NoteContent, SNNote, FillItemContent, DecryptedPayload, PayloadTimestampDefaults } from '@standardnotes/models' -import { ContentType } from '@standardnotes/common' -import { EncryptionService, InternalEventBusInterface } from '@standardnotes/services' import { - ChallengeService, - MutatorService, - PayloadManager, - SNComponentManager, - SNProtectionService, - ItemManager, - SNSyncService, -} from '../' + NoteContent, + SNNote, + FillItemContent, + DecryptedPayload, + PayloadTimestampDefaults, + MutationType, + FileItem, + SNTag, +} from '@standardnotes/models' +import { ContentType } from '@standardnotes/common' +import { AlertService, InternalEventBusInterface } from '@standardnotes/services' +import { MutatorService, PayloadManager, ItemManager } from '../' import { UuidGenerator } from '@standardnotes/utils' const setupRandomUuid = () => { @@ -21,12 +21,6 @@ describe('mutator service', () => { let mutatorService: MutatorService let payloadManager: PayloadManager let itemManager: ItemManager - let syncService: SNSyncService - let protectionService: SNProtectionService - let protocolService: EncryptionService - let challengeService: ChallengeService - let componentManager: SNComponentManager - let historyService: SNHistoryManager let internalEventBus: InternalEventBusInterface @@ -38,17 +32,10 @@ describe('mutator service', () => { payloadManager = new PayloadManager(internalEventBus) itemManager = new ItemManager(payloadManager, internalEventBus) - mutatorService = new MutatorService( - itemManager, - syncService, - protectionService, - protocolService, - payloadManager, - challengeService, - componentManager, - historyService, - internalEventBus, - ) + const alerts = {} as jest.Mocked + alerts.alert = jest.fn() + + mutatorService = new MutatorService(itemManager, payloadManager, alerts, internalEventBus) }) const insertNote = (title: string) => { @@ -73,10 +60,76 @@ describe('mutator service', () => { (mutator) => { mutator.pinned = true }, - false, + MutationType.NoUpdateUserTimestamps, ) expect(note.userModifiedDate).toEqual(pinnedNote?.userModifiedDate) }) }) + + describe('linking', () => { + it('attempting to link file and note should not be allowed if items belong to different vaults', async () => { + const note = { + uuid: 'note', + key_system_identifier: '123', + } as jest.Mocked + + const file = { + uuid: 'file', + key_system_identifier: '456', + } as jest.Mocked + + const result = await mutatorService.associateFileWithNote(file, note) + + expect(result).toBeUndefined() + }) + + it('attempting to link vaulted tag with non vaulted note should not be permissable', async () => { + const note = { + uuid: 'note', + key_system_identifier: undefined, + } as jest.Mocked + + const tag = { + uuid: 'tag', + key_system_identifier: '456', + } as jest.Mocked + + const result = await mutatorService.addTagToNote(note, tag, true) + + expect(result).toBeUndefined() + }) + + it('attempting to link vaulted tag with non vaulted file should not be permissable', async () => { + const tag = { + uuid: 'tag', + key_system_identifier: '456', + } as jest.Mocked + + const file = { + uuid: 'file', + key_system_identifier: undefined, + } as jest.Mocked + + const result = await mutatorService.addTagToFile(file, tag, true) + + expect(result).toBeUndefined() + }) + + it('attempting to link vaulted tag with note belonging to different vault should not be perpermissable', async () => { + const note = { + uuid: 'note', + key_system_identifier: '123', + } as jest.Mocked + + const tag = { + uuid: 'tag', + key_system_identifier: '456', + } as jest.Mocked + + const result = await mutatorService.addTagToNote(note, tag, true) + + expect(result).toBeUndefined() + }) + }) }) diff --git a/packages/snjs/lib/Services/Mutator/MutatorService.ts b/packages/snjs/lib/Services/Mutator/MutatorService.ts index 1407c5777..1a74f1530 100644 --- a/packages/snjs/lib/Services/Mutator/MutatorService.ts +++ b/packages/snjs/lib/Services/Mutator/MutatorService.ts @@ -1,62 +1,60 @@ -import { SNHistoryManager } from './../History/HistoryManager' import { AbstractService, InternalEventBusInterface, - SyncOptions, - ChallengeValidation, - ChallengePrompt, - ChallengeReason, MutatorClientInterface, - Challenge, - InfoStrings, + ItemRelationshipDirection, + AlertService, } from '@standardnotes/services' -import { EncryptionProviderInterface } from '@standardnotes/encryption' -import { ClientDisplayableError } from '@standardnotes/responses' -import { ContentType, ProtocolVersion, compareVersions } from '@standardnotes/common' +import { ItemsKeyMutator, SNItemsKey } from '@standardnotes/encryption' +import { ContentType } from '@standardnotes/common' import { ItemManager } from '../Items' import { PayloadManager } from '../Payloads/PayloadManager' -import { SNComponentManager } from '../ComponentManager/ComponentManager' -import { SNProtectionService } from '../Protection/ProtectionService' -import { SNSyncService } from '../Sync' -import { Strings } from '../../Strings' import { TagsToFoldersMigrationApplicator } from '@Lib/Migrations/Applicators/TagsToFolders' -import { ChallengeService } from '../Challenge' import { - BackupFile, - BackupFileDecryptedContextualPayload, - ComponentContent, - CopyPayloadWithContentOverride, - CreateDecryptedBackupFileContextPayload, + ActionsExtensionMutator, + ComponentMutator, CreateDecryptedMutatorForItem, - CreateEncryptedBackupFileContextPayload, DecryptedItemInterface, DecryptedItemMutator, DecryptedPayload, DecryptedPayloadInterface, + DeleteItemMutator, EncryptedItemInterface, + FeatureRepoMutator, FileItem, - isDecryptedPayload, - isEncryptedTransferPayload, + FileMutator, + FillItemContent, ItemContent, + ItemsKeyInterface, + ItemsKeyMutatorInterface, MutationType, + NoteMutator, PayloadEmitSource, + PayloadsByDuplicating, + PayloadTimestampDefaults, + PayloadVaultOverrides, + predicateFromDSLString, + PredicateInterface, SmartView, + SmartViewContent, + SmartViewDefaultIconName, + SNActionsExtension, SNComponent, + SNFeatureRepo, SNNote, SNTag, + TagContent, + TagMutator, TransactionalMutation, + VaultListingInterface, } from '@standardnotes/models' +import { UuidGenerator, Uuids } from '@standardnotes/utils' export class MutatorService extends AbstractService implements MutatorClientInterface { constructor( private itemManager: ItemManager, - private syncService: SNSyncService, - private protectionService: SNProtectionService, - private encryption: EncryptionProviderInterface, private payloadManager: PayloadManager, - private challengeService: ChallengeService, - private componentManager: SNComponentManager, - private historyService: SNHistoryManager, + private alerts: AlertService, protected override internalEventBus: InternalEventBusInterface, ) { super(internalEventBus) @@ -65,86 +63,98 @@ export class MutatorService extends AbstractService implements MutatorClientInte override deinit() { super.deinit() ;(this.itemManager as unknown) = undefined - ;(this.syncService as unknown) = undefined - ;(this.protectionService as unknown) = undefined - ;(this.encryption as unknown) = undefined ;(this.payloadManager as unknown) = undefined - ;(this.challengeService as unknown) = undefined - ;(this.componentManager as unknown) = undefined - ;(this.historyService as unknown) = undefined } - public async insertItem(item: DecryptedItemInterface): Promise { - const mutator = CreateDecryptedMutatorForItem(item, MutationType.UpdateUserTimestamps) - const dirtiedPayload = mutator.getResult() - const insertedItem = await this.itemManager.emitItemFromPayload(dirtiedPayload, PayloadEmitSource.LocalInserted) - return insertedItem - } - - public async changeAndSaveItem( - itemToLookupUuidFor: DecryptedItemInterface, - mutate: (mutator: M) => void, - updateTimestamps = true, - emitSource?: PayloadEmitSource, - syncOptions?: SyncOptions, - ): Promise { - await this.itemManager.changeItems( + /** + * Consumers wanting to modify an item should run it through this block, + * so that data is properly mapped through our function, and latest state + * is properly reconciled. + */ + public async changeItem< + M extends DecryptedItemMutator = DecryptedItemMutator, + I extends DecryptedItemInterface = DecryptedItemInterface, + >( + itemToLookupUuidFor: I, + mutate?: (mutator: M) => void, + mutationType: MutationType = MutationType.UpdateUserTimestamps, + emitSource = PayloadEmitSource.LocalChanged, + payloadSourceKey?: string, + ): Promise { + const results = await this.changeItems( [itemToLookupUuidFor], mutate, - updateTimestamps ? MutationType.UpdateUserTimestamps : MutationType.NoUpdateUserTimestamps, + mutationType, emitSource, + payloadSourceKey, ) - await this.syncService.sync(syncOptions) - return this.itemManager.findItem(itemToLookupUuidFor.uuid) + return results[0] } - public async changeAndSaveItems( - itemsToLookupUuidsFor: DecryptedItemInterface[], - mutate: (mutator: M) => void, - updateTimestamps = true, - emitSource?: PayloadEmitSource, - syncOptions?: SyncOptions, - ): Promise { - await this.itemManager.changeItems( - itemsToLookupUuidsFor, - mutate, - updateTimestamps ? MutationType.UpdateUserTimestamps : MutationType.NoUpdateUserTimestamps, - emitSource, - ) - await this.syncService.sync(syncOptions) - } - - public async changeItem( - itemToLookupUuidFor: DecryptedItemInterface, - mutate: (mutator: M) => void, - updateTimestamps = true, - ): Promise { - await this.itemManager.changeItems( - [itemToLookupUuidFor], - mutate, - updateTimestamps ? MutationType.UpdateUserTimestamps : MutationType.NoUpdateUserTimestamps, - ) - return this.itemManager.findItem(itemToLookupUuidFor.uuid) - } - - public async changeItems( - itemsToLookupUuidsFor: DecryptedItemInterface[], - mutate: (mutator: M) => void, - updateTimestamps = true, - ): Promise<(DecryptedItemInterface | undefined)[]> { - return this.itemManager.changeItems( - itemsToLookupUuidsFor, - mutate, - updateTimestamps ? MutationType.UpdateUserTimestamps : MutationType.NoUpdateUserTimestamps, - ) + /** + * @param mutate If not supplied, the intention would simply be to mark the item as dirty. + */ + public async changeItems< + M extends DecryptedItemMutator = DecryptedItemMutator, + I extends DecryptedItemInterface = DecryptedItemInterface, + >( + itemsToLookupUuidsFor: I[], + mutate?: (mutator: M) => void, + mutationType: MutationType = MutationType.UpdateUserTimestamps, + emitSource = PayloadEmitSource.LocalChanged, + payloadSourceKey?: string, + ): Promise { + const items = this.itemManager.findItemsIncludingBlanks(Uuids(itemsToLookupUuidsFor)) + const payloads: DecryptedPayloadInterface[] = [] + + for (const item of items) { + if (!item) { + throw Error('Attempting to change non-existant item') + } + const mutator = CreateDecryptedMutatorForItem(item, mutationType) + if (mutate) { + mutate(mutator as M) + } + const payload = mutator.getResult() + payloads.push(payload) + } + + await this.payloadManager.emitPayloads(payloads, emitSource, payloadSourceKey) + + const results = this.itemManager.findItems(payloads.map((p) => p.uuid)) as I[] + + return results } + /** + * Run unique mutations per each item in the array, then only propagate all changes + * once all mutations have been run. This differs from `changeItems` in that changeItems + * runs the same mutation on all items. + */ public async runTransactionalMutations( transactions: TransactionalMutation[], emitSource = PayloadEmitSource.LocalChanged, payloadSourceKey?: string, ): Promise<(DecryptedItemInterface | undefined)[]> { - return this.itemManager.runTransactionalMutations(transactions, emitSource, payloadSourceKey) + const payloads: DecryptedPayloadInterface[] = [] + + for (const transaction of transactions) { + const item = this.itemManager.findItem(transaction.itemUuid) + + if (!item) { + continue + } + + const mutator = CreateDecryptedMutatorForItem(item, transaction.mutationType || MutationType.UpdateUserTimestamps) + + transaction.mutate(mutator) + const payload = mutator.getResult() + payloads.push(payload) + } + + await this.payloadManager.emitPayloads(payloads, emitSource, payloadSourceKey) + const results = this.itemManager.findItems(payloads.map((p) => p.uuid)) + return results } public async runTransactionalMutation( @@ -152,97 +162,387 @@ export class MutatorService extends AbstractService implements MutatorClientInte emitSource = PayloadEmitSource.LocalChanged, payloadSourceKey?: string, ): Promise { - return this.itemManager.runTransactionalMutation(transaction, emitSource, payloadSourceKey) + const item = this.itemManager.findSureItem(transaction.itemUuid) + const mutator = CreateDecryptedMutatorForItem(item, transaction.mutationType || MutationType.UpdateUserTimestamps) + transaction.mutate(mutator) + const payload = mutator.getResult() + + await this.payloadManager.emitPayloads([payload], emitSource, payloadSourceKey) + const result = this.itemManager.findItem(payload.uuid) + return result } - async protectItems(items: I[]): Promise { - const protectedItems = await this.itemManager.changeItems( - items, - (mutator) => { - mutator.protected = true - }, - MutationType.NoUpdateUserTimestamps, - ) + async changeNote( + itemToLookupUuidFor: SNNote, + mutate: (mutator: NoteMutator) => void, + mutationType: MutationType = MutationType.UpdateUserTimestamps, + emitSource = PayloadEmitSource.LocalChanged, + payloadSourceKey?: string, + ): Promise { + const note = this.itemManager.findItem(itemToLookupUuidFor.uuid) + if (!note) { + throw Error('Attempting to change non-existant note') + } + const mutator = new NoteMutator(note, mutationType) - void this.syncService.sync() - return protectedItems + return this.applyTransform(mutator, mutate, emitSource, payloadSourceKey) } - async unprotectItems( - items: I[], - reason: ChallengeReason, - ): Promise { - if ( - !(await this.protectionService.authorizeAction(reason, { - fallBackToAccountPassword: true, - requireAccountPassword: false, - forcePrompt: false, - })) - ) { - return undefined + async changeTag( + itemToLookupUuidFor: SNTag, + mutate: (mutator: TagMutator) => void, + mutationType: MutationType = MutationType.UpdateUserTimestamps, + emitSource = PayloadEmitSource.LocalChanged, + payloadSourceKey?: string, + ): Promise { + const tag = this.itemManager.findItem(itemToLookupUuidFor.uuid) + if (!tag) { + throw Error('Attempting to change non-existant tag') + } + const mutator = new TagMutator(tag, mutationType) + await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey) + return this.itemManager.findSureItem(itemToLookupUuidFor.uuid) + } + + async changeComponent( + itemToLookupUuidFor: SNComponent, + mutate: (mutator: ComponentMutator) => void, + mutationType: MutationType = MutationType.UpdateUserTimestamps, + emitSource = PayloadEmitSource.LocalChanged, + payloadSourceKey?: string, + ): Promise { + const component = this.itemManager.findItem(itemToLookupUuidFor.uuid) + if (!component) { + throw Error('Attempting to change non-existant component') + } + const mutator = new ComponentMutator(component, mutationType) + await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey) + return this.itemManager.findSureItem(itemToLookupUuidFor.uuid) + } + + async changeFeatureRepo( + itemToLookupUuidFor: SNFeatureRepo, + mutate: (mutator: FeatureRepoMutator) => void, + mutationType: MutationType = MutationType.UpdateUserTimestamps, + emitSource = PayloadEmitSource.LocalChanged, + payloadSourceKey?: string, + ): Promise { + const repo = this.itemManager.findItem(itemToLookupUuidFor.uuid) + if (!repo) { + throw Error('Attempting to change non-existant repo') + } + const mutator = new FeatureRepoMutator(repo, mutationType) + await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey) + return this.itemManager.findSureItem(itemToLookupUuidFor.uuid) + } + + async changeActionsExtension( + itemToLookupUuidFor: SNActionsExtension, + mutate: (mutator: ActionsExtensionMutator) => void, + mutationType: MutationType = MutationType.UpdateUserTimestamps, + emitSource = PayloadEmitSource.LocalChanged, + payloadSourceKey?: string, + ): Promise { + const extension = this.itemManager.findItem(itemToLookupUuidFor.uuid) + if (!extension) { + throw Error('Attempting to change non-existant extension') + } + const mutator = new ActionsExtensionMutator(extension, mutationType) + await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey) + return this.itemManager.findSureItem(itemToLookupUuidFor.uuid) + } + + async changeItemsKey( + itemToLookupUuidFor: ItemsKeyInterface, + mutate: (mutator: ItemsKeyMutatorInterface) => void, + mutationType: MutationType = MutationType.UpdateUserTimestamps, + emitSource = PayloadEmitSource.LocalChanged, + payloadSourceKey?: string, + ): Promise { + const itemsKey = this.itemManager.findItem(itemToLookupUuidFor.uuid) + + if (!itemsKey) { + throw Error('Attempting to change non-existant itemsKey') } - const unprotectedItems = await this.itemManager.changeItems( - items, - (mutator) => { - mutator.protected = false - }, - MutationType.NoUpdateUserTimestamps, + const mutator = new ItemsKeyMutator(itemsKey, mutationType) + + await this.applyTransform(mutator, mutate, emitSource, payloadSourceKey) + + return this.itemManager.findSureItem(itemToLookupUuidFor.uuid) + } + + private async applyTransform( + mutator: T, + mutate: (mutator: T) => void, + emitSource = PayloadEmitSource.LocalChanged, + payloadSourceKey?: string, + ): Promise { + mutate(mutator) + const payload = mutator.getResult() + return this.payloadManager.emitPayload(payload, emitSource, payloadSourceKey) + } + + /** + * Sets the item as needing sync. The item is then run through the mapping function, + * and propagated to mapping observers. + * @param isUserModified - Whether to update the item's "user modified date" + */ + public async setItemDirty(itemToLookupUuidFor: DecryptedItemInterface, isUserModified = false) { + const result = await this.setItemsDirty([itemToLookupUuidFor], isUserModified) + return result[0] + } + + public async setItemsDirty( + itemsToLookupUuidsFor: DecryptedItemInterface[], + isUserModified = false, + ): Promise { + return this.changeItems( + itemsToLookupUuidsFor, + undefined, + isUserModified ? MutationType.UpdateUserTimestamps : MutationType.NoUpdateUserTimestamps, + ) + } + + /** + * Duplicates an item and maps it, thus propagating the item to observers. + * @param isConflict - Whether to mark the duplicate as a conflict of the original. + */ + public async duplicateItem( + itemToLookupUuidFor: T, + isConflict = false, + additionalContent?: Partial, + ) { + const item = this.itemManager.findSureItem(itemToLookupUuidFor.uuid) + const payload = item.payload.copy() + const resultingPayloads = PayloadsByDuplicating({ + payload, + baseCollection: this.payloadManager.getMasterCollection(), + isConflict, + additionalContent, + }) + + await this.payloadManager.emitPayloads(resultingPayloads, PayloadEmitSource.LocalChanged) + const duplicate = this.itemManager.findSureItem(resultingPayloads[0].uuid) + + return duplicate + } + + public async createItem( + contentType: ContentType, + content: C, + needsSync = false, + vault?: VaultListingInterface, + ): Promise { + const payload = new DecryptedPayload({ + uuid: UuidGenerator.GenerateUuid(), + content_type: contentType, + content: FillItemContent(content), + dirty: needsSync, + ...PayloadVaultOverrides(vault), + ...PayloadTimestampDefaults(), + }) + + await this.payloadManager.emitPayload(payload, PayloadEmitSource.LocalInserted) + + return this.itemManager.findSureItem(payload.uuid) + } + + public async insertItem(item: DecryptedItemInterface, setDirty = true): Promise { + if (setDirty) { + const mutator = CreateDecryptedMutatorForItem(item, MutationType.UpdateUserTimestamps) + const dirtiedPayload = mutator.getResult() + const insertedItem = await this.emitItemFromPayload(dirtiedPayload, PayloadEmitSource.LocalInserted) + return insertedItem + } else { + return this.emitItemFromPayload(item.payload, PayloadEmitSource.LocalChanged) + } + } + + public async insertItems( + items: DecryptedItemInterface[], + emitSource: PayloadEmitSource = PayloadEmitSource.LocalInserted, + ): Promise { + return this.emitItemsFromPayloads( + items.map((item) => item.payload), + emitSource, + ) + } + + public async emitItemFromPayload( + payload: DecryptedPayloadInterface, + emitSource: PayloadEmitSource, + ): Promise { + await this.payloadManager.emitPayload(payload, emitSource) + + const result = this.itemManager.findSureItem(payload.uuid) + + if (!result) { + throw Error("Emitted item can't be found") + } + + return result + } + + public async emitItemsFromPayloads( + payloads: DecryptedPayloadInterface[], + emitSource: PayloadEmitSource, + ): Promise { + await this.payloadManager.emitPayloads(payloads, emitSource) + + const uuids = Uuids(payloads) + + return this.itemManager.findItems(uuids) + } + + public async setItemToBeDeleted( + itemToLookupUuidFor: DecryptedItemInterface | EncryptedItemInterface, + source: PayloadEmitSource = PayloadEmitSource.LocalChanged, + ): Promise { + const referencingIdsCapturedBeforeChanges = this.itemManager + .getCollection() + .uuidsThatReferenceUuid(itemToLookupUuidFor.uuid) + + const item = this.itemManager.findAnyItem(itemToLookupUuidFor.uuid) + if (!item) { + return + } + + const mutator = new DeleteItemMutator(item, MutationType.UpdateUserTimestamps) + + const deletedPayload = mutator.getDeletedResult() + + await this.payloadManager.emitPayload(deletedPayload, source) + + for (const referencingId of referencingIdsCapturedBeforeChanges) { + const referencingItem = this.itemManager.findItem(referencingId) + + if (referencingItem) { + await this.changeItem(referencingItem, (mutator) => { + mutator.removeItemAsRelationship(item) + }) + } + } + } + + public async setItemsToBeDeleted( + itemsToLookupUuidsFor: (DecryptedItemInterface | EncryptedItemInterface)[], + ): Promise { + await Promise.all(itemsToLookupUuidsFor.map((item) => this.setItemToBeDeleted(item))) + } + + public async findOrCreateTagParentChain(titlesHierarchy: string[]): Promise { + let current: SNTag | undefined = undefined + + for (const title of titlesHierarchy) { + current = await this.findOrCreateTagByTitle({ title, parentItemToLookupUuidFor: current }) + } + + if (!current) { + throw new Error('Invalid tag hierarchy') + } + + return current + } + + public async createTag(dto: { + title: string + parentItemToLookupUuidFor?: SNTag + createInVault?: VaultListingInterface + }): Promise { + const newTag = await this.createItem( + ContentType.Tag, + FillItemContent({ title: dto.title }), + true, + dto.createInVault, ) - void this.syncService.sync() - return unprotectedItems + if (dto.parentItemToLookupUuidFor) { + const parentTag = this.itemManager.findItem(dto.parentItemToLookupUuidFor.uuid) + if (!parentTag) { + throw new Error('Invalid parent tag') + } + return this.changeTag(newTag, (m) => { + m.makeChildOf(parentTag) + }) + } + + return newTag } - public async protectNote(note: SNNote): Promise { - const result = await this.protectItems([note]) - return result[0] + public async createSmartView(dto: { + title: string + predicate: PredicateInterface + iconString?: string + vault?: VaultListingInterface + }): Promise { + return this.createItem( + ContentType.SmartView, + FillItemContent({ + title: dto.title, + predicate: dto.predicate.toJson(), + iconString: dto.iconString || SmartViewDefaultIconName, + } as SmartViewContent), + true, + dto.vault, + ) as Promise } - public async unprotectNote(note: SNNote): Promise { - const result = await this.unprotectItems([note], ChallengeReason.UnprotectNote) - return result ? result[0] : undefined + public async createSmartViewFromDSL( + dsl: string, + vault?: VaultListingInterface, + ): Promise { + let components = null + try { + components = JSON.parse(dsl.substring(1, dsl.length)) + } catch (e) { + throw Error('Invalid smart view syntax') + } + + const title = components[0] + const predicate = predicateFromDSLString(dsl) + return this.createSmartView({ title, predicate, vault }) } - public async protectNotes(notes: SNNote[]): Promise { - return this.protectItems(notes) + public async createTagOrSmartView( + title: string, + vault?: VaultListingInterface, + ): Promise { + if (this.itemManager.isSmartViewTitle(title)) { + return this.createSmartViewFromDSL(title, vault) as Promise + } else { + return this.createTag({ title, createInVault: vault }) as Promise + } } - public async unprotectNotes(notes: SNNote[]): Promise { - const results = await this.unprotectItems(notes, ChallengeReason.UnprotectNote) - return results || [] + public async findOrCreateTagByTitle(dto: { + title: string + parentItemToLookupUuidFor?: SNTag + createInVault?: VaultListingInterface + }): Promise { + const tag = this.itemManager.findTagByTitleAndParent(dto.title, dto.parentItemToLookupUuidFor) + return tag || this.createTag(dto) } - async protectFile(file: FileItem): Promise { - const result = await this.protectItems([file]) - return result[0] - } - - async unprotectFile(file: FileItem): Promise { - const result = await this.unprotectItems([file], ChallengeReason.UnprotectFile) - return result ? result[0] : undefined + public renameFile(file: FileItem, name: string): Promise { + return this.changeItem(file, (mutator) => { + mutator.name = name + }) } public async mergeItem(item: DecryptedItemInterface, source: PayloadEmitSource): Promise { - return this.itemManager.emitItemFromPayload(item.payloadRepresentation(), source) - } - - public createTemplateItem< - C extends ItemContent = ItemContent, - I extends DecryptedItemInterface = DecryptedItemInterface, - >(contentType: ContentType, content?: C, override?: Partial>): I { - return this.itemManager.createTemplateItem(contentType, content, override) + return this.emitItemFromPayload(item.payloadRepresentation(), source) } public async setItemNeedsSync( item: DecryptedItemInterface, updateTimestamps = false, ): Promise { - return this.itemManager.setItemDirty(item, updateTimestamps) + return this.setItemDirty(item, updateTimestamps) } public async setItemsNeedsSync(items: DecryptedItemInterface[]): Promise<(DecryptedItemInterface | undefined)[]> { - return this.itemManager.setItemsDirty(items) + return this.setItemsDirty(items) } public async deleteItem(item: DecryptedItemInterface | EncryptedItemInterface): Promise { @@ -250,153 +550,150 @@ export class MutatorService extends AbstractService implements MutatorClientInte } public async deleteItems(items: (DecryptedItemInterface | EncryptedItemInterface)[]): Promise { - await this.itemManager.setItemsToBeDeleted(items) - await this.syncService.sync() + await this.setItemsToBeDeleted(items) } + /** + * Permanently deletes any items currently in the trash. Consumer must manually call sync. + */ public async emptyTrash(): Promise { - await this.itemManager.emptyTrash() - await this.syncService.sync() + const notes = this.itemManager.trashedItems + await this.setItemsToBeDeleted(notes) } - public duplicateItem( - item: T, - additionalContent?: Partial, - ): Promise { - const duplicate = this.itemManager.duplicateItem(item, false, additionalContent) - void this.syncService.sync() - return duplicate + public async migrateTagsToFolders(): Promise { + await TagsToFoldersMigrationApplicator.run(this.itemManager, this) } - public async migrateTagsToFolders(): Promise { - await TagsToFoldersMigrationApplicator.run(this.itemManager) - return this.syncService.sync() + public async findOrCreateTag(title: string, createInVault?: VaultListingInterface): Promise { + return this.findOrCreateTagByTitle({ title, createInVault }) } - public async setTagParent(parentTag: SNTag, childTag: SNTag): Promise { - await this.itemManager.setTagParent(parentTag, childTag) - } - - public async unsetTagParent(childTag: SNTag): Promise { - await this.itemManager.unsetTagParent(childTag) - } - - public async findOrCreateTag(title: string): Promise { - return this.itemManager.findOrCreateTagByTitle(title) - } - - /** Creates and returns the tag but does not run sync. Callers must perform sync. */ - public async createTagOrSmartView(title: string): Promise { - return this.itemManager.createTagOrSmartView(title) - } - - public async toggleComponent(component: SNComponent): Promise { - await this.componentManager.toggleComponent(component.uuid) - await this.syncService.sync() - } - - public async toggleTheme(theme: SNComponent): Promise { - await this.componentManager.toggleTheme(theme.uuid) - await this.syncService.sync() - } - - public async importData( - data: BackupFile, - awaitSync = false, - ): Promise< - | { - affectedItems: DecryptedItemInterface[] - errorCount: number - } - | { - error: ClientDisplayableError - } - > { - if (data.version) { - /** - * Prior to 003 backup files did not have a version field so we cannot - * stop importing if there is no backup file version, only if there is - * an unsupported version. - */ - const version = data.version as ProtocolVersion - - const supportedVersions = this.encryption.supportedVersions() - if (!supportedVersions.includes(version)) { - return { error: new ClientDisplayableError(InfoStrings.UnsupportedBackupFileVersion) } - } - - const userVersion = this.encryption.getUserVersion() - if (userVersion && compareVersions(version, userVersion) === 1) { - /** File was made with a greater version than the user's account */ - return { error: new ClientDisplayableError(InfoStrings.BackupFileMoreRecentThanAccount) } - } + /** + * @returns The changed child tag + */ + public async setTagParent(parentTag: SNTag, childTag: SNTag): Promise { + if (parentTag.uuid === childTag.uuid) { + throw new Error('Can not set a tag parent of itself') } - let password: string | undefined - - if (data.auth_params || data.keyParams) { - /** Get import file password. */ - const challenge = new Challenge( - [new ChallengePrompt(ChallengeValidation.None, Strings.Input.FileAccountPassword, undefined, true)], - ChallengeReason.DecryptEncryptedFile, - true, - ) - const passwordResponse = await this.challengeService.promptForChallengeResponse(challenge) - if (passwordResponse == undefined) { - /** Challenge was canceled */ - return { error: new ClientDisplayableError('Import aborted') } - } - this.challengeService.completeChallenge(challenge) - password = passwordResponse?.values[0].value as string + if (this.itemManager.isTagAncestor(childTag, parentTag)) { + throw new Error('Can not set a tag ancestor of itself') } - if (!(await this.protectionService.authorizeFileImport())) { - return { error: new ClientDisplayableError('Import aborted') } - } - - data.items = data.items.map((item) => { - if (isEncryptedTransferPayload(item)) { - return CreateEncryptedBackupFileContextPayload(item) - } else { - return CreateDecryptedBackupFileContextPayload(item as BackupFileDecryptedContextualPayload) - } + return this.changeTag(childTag, (m) => { + m.makeChildOf(parentTag) }) + } - const decryptedPayloadsOrError = await this.encryption.decryptBackupFile(data, password) + /** + * @returns The changed child tag + */ + public unsetTagParent(childTag: SNTag): Promise { + const parentTag = this.itemManager.getTagParent(childTag) - if (decryptedPayloadsOrError instanceof ClientDisplayableError) { - return { error: decryptedPayloadsOrError } + if (!parentTag) { + return Promise.resolve(childTag) } - const validPayloads = decryptedPayloadsOrError.filter(isDecryptedPayload).map((payload) => { - /* Don't want to activate any components during import process in - * case of exceptions breaking up the import proccess */ - if (payload.content_type === ContentType.Component && (payload.content as ComponentContent).active) { - const typedContent = payload as DecryptedPayloadInterface - return CopyPayloadWithContentOverride(typedContent, { - active: false, - }) - } else { - return payload - } + return this.changeTag(childTag, (m) => { + m.unsetParent() }) + } - const affectedUuids = await this.payloadManager.importPayloads( - validPayloads, - this.historyService.getHistoryMapCopy(), + public async associateFileWithNote(file: FileItem, note: SNNote): Promise { + const isVaultConflict = + file.key_system_identifier && + note.key_system_identifier && + file.key_system_identifier !== note.key_system_identifier + + if (isVaultConflict) { + void this.alerts.alert('The items you are trying to link belong to different vaults and cannot be linked') + return undefined + } + + return this.changeItem(file, (mutator) => { + mutator.addNote(note) + }) + } + + public async disassociateFileWithNote(file: FileItem, note: SNNote): Promise { + return this.changeItem(file, (mutator) => { + mutator.removeNote(note) + }) + } + + public async addTagToNote(note: SNNote, tag: SNTag, addHierarchy: boolean): Promise { + if (tag.key_system_identifier !== note.key_system_identifier) { + void this.alerts.alert('The items you are trying to link belong to different vaults and cannot be linked') + return undefined + } + + let tagsToAdd = [tag] + + if (addHierarchy) { + const parentChainTags = this.itemManager.getTagParentChain(tag) + tagsToAdd = [...parentChainTags, tag] + } + + return Promise.all( + tagsToAdd.map((tagToAdd) => { + return this.changeTag(tagToAdd, (mutator) => { + mutator.addNote(note) + }) as Promise + }), ) + } - const promise = this.syncService.sync() - - if (awaitSync) { - await promise + public async addTagToFile(file: FileItem, tag: SNTag, addHierarchy: boolean): Promise { + if (tag.key_system_identifier !== file.key_system_identifier) { + void this.alerts.alert('The items you are trying to link belong to different vaults and cannot be linked') + return undefined } - const affectedItems = this.itemManager.findItems(affectedUuids) as DecryptedItemInterface[] + let tagsToAdd = [tag] - return { - affectedItems: affectedItems, - errorCount: decryptedPayloadsOrError.length - validPayloads.length, + if (addHierarchy) { + const parentChainTags = this.itemManager.getTagParentChain(tag) + tagsToAdd = [...parentChainTags, tag] } + + return Promise.all( + tagsToAdd.map((tagToAdd) => { + return this.changeTag(tagToAdd, (mutator) => { + mutator.addFile(file) + }) as Promise + }), + ) + } + + public async linkNoteToNote(note: SNNote, otherNote: SNNote): Promise { + return this.changeItem(note, (mutator) => { + mutator.addNote(otherNote) + }) + } + + public async linkFileToFile(file: FileItem, otherFile: FileItem): Promise { + return this.changeItem(file, (mutator) => { + mutator.addFile(otherFile) + }) + } + + public async unlinkItems( + itemA: DecryptedItemInterface, + itemB: DecryptedItemInterface, + ): Promise> { + const relationshipDirection = this.itemManager.relationshipDirectionBetweenItems(itemA, itemB) + + if (relationshipDirection === ItemRelationshipDirection.NoRelationship) { + throw new Error('Trying to unlink already unlinked items') + } + + const itemToChange = relationshipDirection === ItemRelationshipDirection.AReferencesB ? itemA : itemB + const itemToRemove = itemToChange === itemA ? itemB : itemA + + return this.changeItem(itemToChange, (mutator) => { + mutator.removeItemAsRelationship(itemToRemove) + }) } } diff --git a/packages/snjs/lib/Services/Payloads/PayloadManager.ts b/packages/snjs/lib/Services/Payloads/PayloadManager.ts index 6ab84667b..bc83ebf1f 100644 --- a/packages/snjs/lib/Services/Payloads/PayloadManager.ts +++ b/packages/snjs/lib/Services/Payloads/PayloadManager.ts @@ -300,7 +300,7 @@ export class PayloadManager extends AbstractService implements PayloadManagerInt return Uuids(payloads) } - public removePayloadLocally(payload: FullyFormedPayloadInterface) { + public removePayloadLocally(payload: FullyFormedPayloadInterface | FullyFormedPayloadInterface[]): void { this.collection.discard(payload) } diff --git a/packages/snjs/lib/Services/Preferences/PreferencesService.ts b/packages/snjs/lib/Services/Preferences/PreferencesService.ts index 49a7f1166..5ec0a3c4a 100644 --- a/packages/snjs/lib/Services/Preferences/PreferencesService.ts +++ b/packages/snjs/lib/Services/Preferences/PreferencesService.ts @@ -10,6 +10,7 @@ import { ApplicationStage, PreferenceServiceInterface, PreferencesServiceEvent, + MutatorClientInterface, } from '@standardnotes/services' export class SNPreferencesService @@ -24,7 +25,8 @@ export class SNPreferencesService constructor( private singletonManager: SNSingletonManager, - private itemManager: ItemManager, + itemManager: ItemManager, + private mutator: MutatorClientInterface, private syncService: SNSyncService, protected override internalEventBus: InternalEventBusInterface, ) { @@ -45,7 +47,7 @@ export class SNPreferencesService this.removeItemObserver?.() this.removeSyncObserver?.() ;(this.singletonManager as unknown) = undefined - ;(this.itemManager as unknown) = undefined + ;(this.mutator as unknown) = undefined super.deinit() } @@ -77,7 +79,7 @@ export class SNPreferencesService return } - this.preferences = (await this.itemManager.changeItem(this.preferences, (m) => { + this.preferences = (await this.mutator.changeItem(this.preferences, (m) => { m.setPref(key, value) })) as SNUserPrefs diff --git a/packages/snjs/lib/Services/Protection/ProtectionService.spec.ts b/packages/snjs/lib/Services/Protection/ProtectionService.spec.ts index fa4eecae6..753b35a65 100644 --- a/packages/snjs/lib/Services/Protection/ProtectionService.spec.ts +++ b/packages/snjs/lib/Services/Protection/ProtectionService.spec.ts @@ -6,6 +6,7 @@ import { InternalEventBusInterface, ChallengeReason, EncryptionService, + MutatorClientInterface, } from '@standardnotes/services' import { UuidGenerator } from '@standardnotes/utils' import { @@ -22,6 +23,7 @@ const setupRandomUuid = () => { } describe('protectionService', () => { + let mutator: MutatorClientInterface let protocolService: EncryptionService let challengeService: ChallengeService let storageService: DiskStorageService @@ -29,7 +31,7 @@ describe('protectionService', () => { let protectionService: SNProtectionService const createService = () => { - return new SNProtectionService(protocolService, challengeService, storageService, internalEventBus) + return new SNProtectionService(protocolService, mutator, challengeService, storageService, internalEventBus) } const createFile = (name: string, isProtected?: boolean) => { @@ -60,6 +62,8 @@ describe('protectionService', () => { protocolService = {} as jest.Mocked protocolService.hasAccount = jest.fn().mockReturnValue(true) protocolService.hasPasscode = jest.fn().mockReturnValue(false) + + mutator = {} as jest.Mocked }) describe('files', () => { diff --git a/packages/snjs/lib/Services/Protection/ProtectionService.ts b/packages/snjs/lib/Services/Protection/ProtectionService.ts index 80fe8dc74..5c3e60977 100644 --- a/packages/snjs/lib/Services/Protection/ProtectionService.ts +++ b/packages/snjs/lib/Services/Protection/ProtectionService.ts @@ -1,6 +1,13 @@ import { ChallengeService } from './../Challenge/ChallengeService' import { SNLog } from '@Lib/Log' -import { DecryptedItem } from '@standardnotes/models' +import { + DecryptedItem, + DecryptedItemInterface, + DecryptedItemMutator, + FileItem, + MutationType, + SNNote, +} from '@standardnotes/models' import { DiskStorageService } from '@Lib/Services/Storage/DiskStorageService' import { isNullOrUndefined } from '@standardnotes/utils' import { @@ -9,7 +16,6 @@ import { StorageValueModes, ApplicationStage, StorageKey, - DiagnosticInfo, Challenge, ChallengeReason, ChallengePrompt, @@ -18,6 +24,7 @@ import { MobileUnlockTiming, TimingDisplayOption, ProtectionsClientInterface, + MutatorClientInterface, } from '@standardnotes/services' import { ContentType } from '@standardnotes/common' @@ -70,6 +77,7 @@ export class SNProtectionService extends AbstractService implem constructor( private protocolService: EncryptionService, + private mutator: MutatorClientInterface, private challengeService: ChallengeService, private storageService: DiskStorageService, protected override internalEventBus: InternalEventBusInterface, @@ -435,15 +443,69 @@ export class SNProtectionService extends AbstractService implem this.sessionExpiryTimeout = setTimeout(timer, expiryDate.getTime() - Date.now()) } - override getDiagnostics(): Promise { - return Promise.resolve({ - protections: { - getSessionExpiryDate: this.getSessionExpiryDate(), - getLastSessionLength: this.getLastSessionLength(), - hasProtectionSources: this.hasProtectionSources(), - hasUnprotectedAccessSession: this.hasUnprotectedAccessSession(), - hasBiometricsEnabled: this.hasBiometricsEnabled(), + async protectItems(items: I[]): Promise { + const protectedItems = await this.mutator.changeItems( + items, + (mutator) => { + mutator.protected = true }, - }) + MutationType.NoUpdateUserTimestamps, + ) + + return protectedItems + } + + async unprotectItems( + items: I[], + reason: ChallengeReason, + ): Promise { + if ( + !(await this.authorizeAction(reason, { + fallBackToAccountPassword: true, + requireAccountPassword: false, + forcePrompt: false, + })) + ) { + return undefined + } + + const unprotectedItems = await this.mutator.changeItems( + items, + (mutator) => { + mutator.protected = false + }, + MutationType.NoUpdateUserTimestamps, + ) + + return unprotectedItems + } + + public async protectNote(note: SNNote): Promise { + const result = await this.protectItems([note]) + return result[0] + } + + public async unprotectNote(note: SNNote): Promise { + const result = await this.unprotectItems([note], ChallengeReason.UnprotectNote) + return result ? result[0] : undefined + } + + public async protectNotes(notes: SNNote[]): Promise { + return this.protectItems(notes) + } + + public async unprotectNotes(notes: SNNote[]): Promise { + const results = await this.unprotectItems(notes, ChallengeReason.UnprotectNote) + return results || [] + } + + async protectFile(file: FileItem): Promise { + const result = await this.protectItems([file]) + return result[0] + } + + async unprotectFile(file: FileItem): Promise { + const result = await this.unprotectItems([file], ChallengeReason.UnprotectFile) + return result ? result[0] : undefined } } diff --git a/packages/snjs/lib/Services/Session/SessionManager.ts b/packages/snjs/lib/Services/Session/SessionManager.ts index e8c2c3191..e185d98e0 100644 --- a/packages/snjs/lib/Services/Session/SessionManager.ts +++ b/packages/snjs/lib/Services/Session/SessionManager.ts @@ -3,7 +3,6 @@ import { AbstractService, InternalEventBusInterface, StorageKey, - DiagnosticInfo, ChallengePrompt, ChallengeValidation, ChallengeKeyboardType, @@ -26,8 +25,12 @@ import { InternalEventInterface, ApiServiceEvent, SessionRefreshedData, + SessionEvent, + UserKeyPairChangedEventData, + InternalFeatureService, + InternalFeature, } from '@standardnotes/services' -import { Base64String } from '@standardnotes/sncrypto-common' +import { Base64String, PkcKeyPair } from '@standardnotes/sncrypto-common' import { ClientDisplayableError, SessionBody, @@ -43,7 +46,7 @@ import { SessionListResponse, HttpSuccessResponse, } from '@standardnotes/responses' -import { CopyPayloadWithContentOverride } from '@standardnotes/models' +import { CopyPayloadWithContentOverride, RootKeyWithKeyPairsInterface } from '@standardnotes/models' import { LegacySession, MapperInterface, Result, Session, SessionToken } from '@standardnotes/domain-core' import { KeyParamsFromApiResponse, SNRootKeyParams, SNRootKey } from '@standardnotes/encryption' import { Subscription } from '@standardnotes/security' @@ -56,7 +59,7 @@ import { DiskStorageService } from '../Storage/DiskStorageService' import { SNWebSocketsService } from '../Api/WebsocketsService' import { Strings } from '@Lib/Strings' import { UuidString } from '@Lib/Types/UuidString' -import { ChallengeService } from '../Challenge' +import { ChallengeResponse, ChallengeService } from '../Challenge' import { ApiCallError, ErrorMessage, @@ -72,11 +75,6 @@ const cleanedEmailString = (email: string) => { return email.trim().toLowerCase() } -export enum SessionEvent { - Restored = 'SessionRestored', - Revoked = 'SessionRevoked', -} - /** * The session manager is responsible for loading initial user state, and any relevant * server credentials, such as the session token. It also exposes methods for registering @@ -139,18 +137,19 @@ export class SNSessionManager } } - private setUser(user?: User) { + private memoizeUser(user?: User) { this.user = user + this.apiService.setUser(user) } async initializeFromDisk() { - this.setUser(this.diskStorageService.getValue(StorageKey.User)) + this.memoizeUser(this.diskStorageService.getValue(StorageKey.User)) if (!this.user) { const legacyUuidLookup = this.diskStorageService.getValue(StorageKey.LegacyUuid) if (legacyUuidLookup) { - this.setUser({ uuid: legacyUuidLookup, email: legacyUuidLookup }) + this.memoizeUser({ uuid: legacyUuidLookup, email: legacyUuidLookup }) } } @@ -193,6 +192,36 @@ export class SNSessionManager return this.user } + public getSureUser(): User { + return this.user as User + } + + isUserMissingKeyPair(): boolean { + try { + return this.getPublicKey() == undefined + } catch (error) { + return true + } + } + + public getPublicKey(): string { + return this.protocolService.getKeyPair().publicKey + } + + public getSigningPublicKey(): string { + return this.protocolService.getSigningKeyPair().publicKey + } + + public get userUuid(): string { + const user = this.getUser() + + if (!user) { + throw Error('Attempting to access userUuid when user is undefined') + } + + return user.uuid + } + isCurrentSessionReadOnly(): boolean | undefined { if (this.session === undefined) { return undefined @@ -205,16 +234,13 @@ export class SNSessionManager return this.session.isReadOnly() } - public getSureUser() { - return this.user as User - } - public getSession() { return this.apiService.getSession() } public async signOut() { - this.setUser(undefined) + this.memoizeUser(undefined) + const session = this.apiService.getSession() if (session && session instanceof Session) { await this.apiService.signOut() @@ -268,7 +294,11 @@ export class SNSessionManager currentKeyParams?.version, ) if (isErrorResponse(response)) { - this.challengeService.setValidationStatusForChallenge(challenge, challengeResponse!.values[1], false) + this.challengeService.setValidationStatusForChallenge( + challenge, + (challengeResponse as ChallengeResponse).values[1], + false, + ) onResponse?.(response) } else { resolve() @@ -373,11 +403,20 @@ export class SNSessionManager email = cleanedEmailString(email) - const rootKey = await this.protocolService.createRootKey(email, password, Common.KeyParamsOrigination.Registration) + const rootKey = await this.protocolService.createRootKey( + email, + password, + Common.KeyParamsOrigination.Registration, + ) const serverPassword = rootKey.serverPassword as string const keyParams = rootKey.keyParams - const registerResponse = await this.userApiService.register({ email, serverPassword, keyParams, ephemeral }) + const registerResponse = await this.userApiService.register({ + email, + serverPassword, + keyParams, + ephemeral, + }) if ('error' in registerResponse.data) { throw new ApiCallError(registerResponse.data.error.message) @@ -485,7 +524,7 @@ export class SNSessionManager response: paramsResult.response, } } - const keyParams = paramsResult.keyParams! + const keyParams = paramsResult.keyParams as SNRootKeyParams if (!this.protocolService.supportedVersions().includes(keyParams.version)) { if (this.protocolService.isVersionNewerThanLibraryVersion(keyParams.version)) { return { @@ -563,7 +602,7 @@ export class SNSessionManager const signInResponse = await this.apiService.signIn({ email, - serverPassword: rootKey.serverPassword!, + serverPassword: rootKey.serverPassword as string, ephemeral, }) @@ -585,20 +624,49 @@ export class SNSessionManager public async changeCredentials(parameters: { currentServerPassword: string - newRootKey: SNRootKey + newRootKey: RootKeyWithKeyPairsInterface wrappingKey?: SNRootKey newEmail?: string }): Promise { - const userUuid = this.user!.uuid - const response = await this.apiService.changeCredentials({ + const userUuid = this.getSureUser().uuid + const rawResponse = await this.apiService.changeCredentials({ userUuid, currentServerPassword: parameters.currentServerPassword, - newServerPassword: parameters.newRootKey.serverPassword!, + newServerPassword: parameters.newRootKey.serverPassword as string, newKeyParams: parameters.newRootKey.keyParams, newEmail: parameters.newEmail, }) - return this.processChangeCredentialsResponse(response, parameters.newRootKey, parameters.wrappingKey) + let oldKeyPair: PkcKeyPair | undefined + let oldSigningKeyPair: PkcKeyPair | undefined + + try { + oldKeyPair = this.protocolService.getKeyPair() + oldSigningKeyPair = this.protocolService.getSigningKeyPair() + } catch (error) { + void error + } + + const processedResponse = await this.processChangeCredentialsResponse( + rawResponse, + parameters.newRootKey, + parameters.wrappingKey, + ) + + if (!isErrorResponse(rawResponse)) { + if (InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults)) { + const eventData: UserKeyPairChangedEventData = { + oldKeyPair, + oldSigningKeyPair, + newKeyPair: parameters.newRootKey.encryptionKeyPair, + newSigningKeyPair: parameters.newRootKey.signingKeyPair, + } + + void this.notifyEvent(SessionEvent.UserKeyPairChanged, eventData) + } + } + + return processedResponse } public async getSessionsList(): Promise> { @@ -669,12 +737,10 @@ export class SNSessionManager ) { await this.protocolService.setRootKey(rootKey, wrappingKey) - this.setUser(user) - + this.memoizeUser(user) this.diskStorageService.setValue(StorageKey.User, user) void this.apiService.setHost(host) - this.httpService.setHost(host) this.setSession(session) @@ -777,16 +843,4 @@ export class SNSessionManager return Result.ok(sessionOrError.getValue()) } - - override getDiagnostics(): Promise { - return Promise.resolve({ - session: { - isSessionRenewChallengePresented: this.isSessionRenewChallengePresented, - online: this.online(), - offline: this.offline(), - isSignedIn: this.isSignedIn(), - isSignedIntoFirstPartyServer: this.isSignedIntoFirstPartyServer(), - }, - }) - } } diff --git a/packages/snjs/lib/Services/Singleton/SingletonManager.ts b/packages/snjs/lib/Services/Singleton/SingletonManager.ts index c37f91c91..ff26b1c57 100644 --- a/packages/snjs/lib/Services/Singleton/SingletonManager.ts +++ b/packages/snjs/lib/Services/Singleton/SingletonManager.ts @@ -10,10 +10,17 @@ import { PayloadEmitSource, PayloadTimestampDefaults, getIncrementedDirtyIndex, + Predicate, } from '@standardnotes/models' import { arrayByRemovingFromIndex, extendArray, UuidGenerator } from '@standardnotes/utils' import { SNSyncService } from '../Sync/SyncService' -import { AbstractService, InternalEventBusInterface, SyncEvent } from '@standardnotes/services' +import { + AbstractService, + InternalEventBusInterface, + MutatorClientInterface, + SingletonManagerInterface, + SyncEvent, +} from '@standardnotes/services' /** * The singleton manager allow consumers to ensure that only 1 item exists of a certain @@ -26,7 +33,7 @@ import { AbstractService, InternalEventBusInterface, SyncEvent } from '@standard * 2. Items can override isSingleton, singletonPredicate, and strategyWhenConflictingWithItem (optional) * to automatically gain singleton resolution. */ -export class SNSingletonManager extends AbstractService { +export class SNSingletonManager extends AbstractService implements SingletonManagerInterface { private resolveQueue: DecryptedItemInterface[] = [] private removeItemObserver!: () => void @@ -34,6 +41,7 @@ export class SNSingletonManager extends AbstractService { constructor( private itemManager: ItemManager, + private mutator: MutatorClientInterface, private payloadManager: PayloadManager, private syncService: SNSyncService, protected override internalEventBus: InternalEventBusInterface, @@ -44,6 +52,7 @@ export class SNSingletonManager extends AbstractService { public override deinit(): void { ;(this.syncService as unknown) = undefined + ;(this.mutator as unknown) = undefined ;(this.itemManager as unknown) = undefined ;(this.payloadManager as unknown) = undefined @@ -148,7 +157,7 @@ export class SNSingletonManager extends AbstractService { }) const deleteItems = arrayByRemovingFromIndex(earliestFirst, 0) - await this.itemManager.setItemsToBeDeleted(deleteItems) + await this.mutator.setItemsToBeDeleted(deleteItems) } public findSingleton( @@ -222,7 +231,66 @@ export class SNSingletonManager extends AbstractService { ...PayloadTimestampDefaults(), }) - const item = await this.itemManager.emitItemFromPayload(dirtyPayload, PayloadEmitSource.LocalInserted) + const item = await this.mutator.emitItemFromPayload(dirtyPayload, PayloadEmitSource.LocalInserted) + + void this.syncService.sync({ sourceDescription: 'After find or create singleton' }) + + return item as T + } + + public async findOrCreateSingleton< + C extends ItemContent = ItemContent, + T extends DecryptedItemInterface = DecryptedItemInterface, + >(predicate: Predicate, contentType: ContentType, createContent: ItemContent): Promise { + const existingItems = this.itemManager.itemsMatchingPredicate(contentType, predicate) + if (existingItems.length > 0) { + return existingItems[0] + } + + /** Item not found, safe to create after full sync has completed */ + if (!this.syncService.getLastSyncDate()) { + /** + * Add a temporary observer in case of long-running sync request, where + * the item we're looking for ends up resolving early or in the middle. + */ + let matchingItem: DecryptedItemInterface | undefined + + const removeObserver = this.itemManager.addObserver(contentType, ({ inserted }) => { + if (inserted.length > 0) { + const matchingItems = inserted.filter((i) => i.satisfiesPredicate(predicate)) + + if (matchingItems.length > 0) { + matchingItem = matchingItems[0] + } + } + }) + + await this.syncService.sync({ sourceDescription: 'Find or create singleton, before any sync has completed' }) + + removeObserver() + + if (matchingItem) { + return matchingItem as T + } + + /** Check again */ + const refreshedItems = this.itemManager.itemsMatchingPredicate(contentType, predicate) + if (refreshedItems.length > 0) { + return refreshedItems[0] as T + } + } + + /** Safe to create */ + const dirtyPayload = new DecryptedPayload({ + uuid: UuidGenerator.GenerateUuid(), + content_type: contentType, + content: createContent, + dirty: true, + dirtyIndex: getIncrementedDirtyIndex(), + ...PayloadTimestampDefaults(), + }) + + const item = await this.mutator.emitItemFromPayload(dirtyPayload, PayloadEmitSource.LocalInserted) void this.syncService.sync({ sourceDescription: 'After find or create singleton' }) diff --git a/packages/snjs/lib/Services/Storage/DiskStorageService.ts b/packages/snjs/lib/Services/Storage/DiskStorageService.ts index e7e77b771..8d386cdb9 100644 --- a/packages/snjs/lib/Services/Storage/DiskStorageService.ts +++ b/packages/snjs/lib/Services/Storage/DiskStorageService.ts @@ -1,10 +1,9 @@ import { ContentType } from '@standardnotes/common' -import { Copy, extendArray, UuidGenerator } from '@standardnotes/utils' +import { Copy, extendArray, UuidGenerator, Uuids } from '@standardnotes/utils' import { SNLog } from '../../Log' import { isErrorDecryptingParameters, SNRootKey } from '@standardnotes/encryption' import * as Encryption from '@standardnotes/encryption' import * as Services from '@standardnotes/services' -import { DiagnosticInfo } from '@standardnotes/services' import { CreateDecryptedLocalStorageContextPayload, CreateDeletedLocalStorageContextPayload, @@ -252,7 +251,7 @@ export class DiskStorageService extends Services.AbstractService implements Serv return rawContent as Services.StorageValuesObject } - public setValue(key: string, value: unknown, mode = Services.StorageValueModes.Default): void { + public setValue(key: string, value: T, mode = Services.StorageValueModes.Default): void { this.setValueWithNoPersist(key, value, mode) void this.persistValuesToDisk() @@ -292,6 +291,14 @@ export class DiskStorageService extends Services.AbstractService implements Serv return value != undefined ? (value as T) : (defaultValue as T) } + public getAllKeys(mode = Services.StorageValueModes.Default): string[] { + if (!this.values) { + throw Error('Attempting to get all keys before loading local storage.') + } + + return Object.keys(this.values[this.domainKeyForMode(mode)]) + } + public async removeValue(key: string, mode = Services.StorageValueModes.Default): Promise { if (!this.values) { throw Error(`Attempting to remove storage key ${key} before loading local storage.`) @@ -370,20 +377,28 @@ export class DiskStorageService extends Services.AbstractService implements Serv const encryptable: DecryptedPayloadInterface[] = [] const unencryptable: DecryptedPayloadInterface[] = [] - const split = Encryption.SplitPayloadsByEncryptionType(decrypted) - if (split.itemsKeyEncryption) { - extendArray(encryptable, split.itemsKeyEncryption) + const { rootKeyEncryption, keySystemRootKeyEncryption, itemsKeyEncryption } = + Encryption.SplitPayloadsByEncryptionType(decrypted) + + if (itemsKeyEncryption) { + extendArray(encryptable, itemsKeyEncryption) } - if (split.rootKeyEncryption) { + if (keySystemRootKeyEncryption) { + extendArray(encryptable, keySystemRootKeyEncryption) + } + + if (rootKeyEncryption) { if (!rootKeyEncryptionAvailable) { - extendArray(unencryptable, split.rootKeyEncryption) + extendArray(unencryptable, rootKeyEncryption) } else { - extendArray(encryptable, split.rootKeyEncryption) + extendArray(encryptable, rootKeyEncryption) } } - await this.deletePayloads(discardable) + if (discardable.length > 0) { + await this.deletePayloads(discardable) + } const encryptableSplit = Encryption.SplitPayloadsByEncryptionType(encryptable) @@ -406,16 +421,18 @@ export class DiskStorageService extends Services.AbstractService implements Serv } public async deletePayloads(payloads: DeletedPayloadInterface[]) { - await Promise.all(payloads.map((payload) => this.deletePayloadWithId(payload.uuid))) + await this.deletePayloadsWithUuids(Uuids(payloads)) } - public async forceDeletePayloads(payloads: FullyFormedPayloadInterface[]) { - await Promise.all(payloads.map((payload) => this.deletePayloadWithId(payload.uuid))) + public async deletePayloadsWithUuids(uuids: string[]): Promise { + await this.executeCriticalFunction(async () => { + await Promise.all(uuids.map((uuid) => this.deviceInterface.removeDatabaseEntry(uuid, this.identifier))) + }) } - public async deletePayloadWithId(uuid: string) { + public async deletePayloadWithUuid(uuid: string) { return this.executeCriticalFunction(async () => { - return this.deviceInterface.removeDatabaseEntry(uuid, this.identifier) + await this.deviceInterface.removeDatabaseEntry(uuid, this.identifier) }) } @@ -437,17 +454,4 @@ export class DiskStorageService extends Services.AbstractService implements Serv await this.deviceInterface.removeRawStorageValue(this.getPersistenceKey()) }) } - - override async getDiagnostics(): Promise { - return { - storage: { - storagePersistable: this.storagePersistable, - persistencePolicy: Services.StoragePersistencePolicies[this.persistencePolicy], - needsPersist: this.needsPersist, - currentPersistPromise: this.currentPersistPromise != undefined, - isStorageWrapped: this.isStorageWrapped(), - allRawPayloadsCount: (await this.getAllRawPayloads()).length, - }, - } - } } diff --git a/packages/snjs/lib/Services/Sync/Account/Operation.ts b/packages/snjs/lib/Services/Sync/Account/Operation.ts index 6d621c2d6..b62f76fe1 100644 --- a/packages/snjs/lib/Services/Sync/Account/Operation.ts +++ b/packages/snjs/lib/Services/Sync/Account/Operation.ts @@ -21,17 +21,15 @@ export class AccountSyncOperation { * @param receiver A function that receives callback multiple times during the operation */ constructor( - private payloads: ServerSyncPushContextualPayload[], + public readonly payloads: ServerSyncPushContextualPayload[], private receiver: ResponseSignalReceiver, - private lastSyncToken: string, - private paginationToken: string, private apiService: SNApiService, + public readonly options: { + syncToken?: string + paginationToken?: string + sharedVaultUuids?: string[] + }, ) { - this.payloads = payloads - this.lastSyncToken = lastSyncToken - this.paginationToken = paginationToken - this.apiService = apiService - this.receiver = receiver this.pendingPayloads = payloads.slice() } @@ -55,13 +53,19 @@ export class AccountSyncOperation { }) const payloads = this.popPayloads(this.upLimit) - const rawResponse = await this.apiService.sync(payloads, this.lastSyncToken, this.paginationToken, this.downLimit) + const rawResponse = await this.apiService.sync( + payloads, + this.options.syncToken, + this.options.paginationToken, + this.downLimit, + this.options.sharedVaultUuids, + ) const response = new ServerSyncResponse(rawResponse) this.responses.push(response) - this.lastSyncToken = response.lastSyncToken as string - this.paginationToken = response.paginationToken as string + this.options.syncToken = response.lastSyncToken as string + this.options.paginationToken = response.paginationToken as string try { await this.receiver(SyncSignal.Response, response) @@ -75,7 +79,7 @@ export class AccountSyncOperation { } get done() { - return this.pendingPayloads.length === 0 && !this.paginationToken + return this.pendingPayloads.length === 0 && !this.options.paginationToken } private get pendingUploadCount() { diff --git a/packages/snjs/lib/Services/Sync/Account/Response.ts b/packages/snjs/lib/Services/Sync/Account/Response.ts index 3732520f4..5e8e02d10 100644 --- a/packages/snjs/lib/Services/Sync/Account/Response.ts +++ b/packages/snjs/lib/Services/Sync/Account/Response.ts @@ -1,27 +1,36 @@ import { ApiEndpointParam, ConflictParams, - ConflictType, + SharedVaultInviteServerHash, + SharedVaultServerHash, HttpError, HttpResponse, isErrorResponse, RawSyncResponse, - ServerItemResponse, + UserEventServerHash, + AsymmetricMessageServerHash, } from '@standardnotes/responses' import { FilterDisallowedRemotePayloadsAndMap, CreateServerSyncSavedPayload, ServerSyncSavedContextualPayload, FilteredServerItem, + TrustedConflictParams, } from '@standardnotes/models' import { deepFreeze } from '@standardnotes/utils' +import { TrustedServerConflictMap } from './ServerConflictMap' export class ServerSyncResponse { - public readonly savedPayloads: ServerSyncSavedContextualPayload[] - public readonly retrievedPayloads: FilteredServerItem[] - public readonly uuidConflictPayloads: FilteredServerItem[] - public readonly dataConflictPayloads: FilteredServerItem[] - public readonly rejectedPayloads: FilteredServerItem[] + readonly savedPayloads: ServerSyncSavedContextualPayload[] + readonly retrievedPayloads: FilteredServerItem[] + readonly conflicts: TrustedServerConflictMap + + readonly asymmetricMessages: AsymmetricMessageServerHash[] + readonly vaults: SharedVaultServerHash[] + readonly vaultInvites: SharedVaultInviteServerHash[] + readonly userEvents: UserEventServerHash[] + + private readonly rawConflictObjects: ConflictParams[] private successResponseData: RawSyncResponse | undefined @@ -32,6 +41,10 @@ export class ServerSyncResponse { this.successResponseData = rawResponse.data } + const conflicts = this.successResponseData?.conflicts || [] + const legacyConflicts = this.successResponseData?.unsaved || [] + this.rawConflictObjects = conflicts.concat(legacyConflicts) + this.savedPayloads = FilterDisallowedRemotePayloadsAndMap(this.successResponseData?.saved_items || []).map( (rawItem) => { return CreateServerSyncSavedPayload(rawItem) @@ -40,15 +53,53 @@ export class ServerSyncResponse { this.retrievedPayloads = FilterDisallowedRemotePayloadsAndMap(this.successResponseData?.retrieved_items || []) - this.dataConflictPayloads = FilterDisallowedRemotePayloadsAndMap(this.rawDataConflictItems) + this.conflicts = this.filterConflicts() - this.uuidConflictPayloads = FilterDisallowedRemotePayloadsAndMap(this.rawUuidConflictItems) + this.vaults = this.successResponseData?.shared_vaults || [] - this.rejectedPayloads = FilterDisallowedRemotePayloadsAndMap(this.rawRejectedPayloads) + this.vaultInvites = this.successResponseData?.shared_vault_invites || [] + + this.asymmetricMessages = this.successResponseData?.asymmetric_messages || [] + + this.userEvents = this.successResponseData?.user_events || [] deepFreeze(this) } + private filterConflicts(): TrustedServerConflictMap { + const conflicts = this.rawConflictObjects + const trustedConflicts: TrustedServerConflictMap = {} + + for (const conflict of conflicts) { + let serverItem: FilteredServerItem | undefined + let unsavedItem: FilteredServerItem | undefined + + if (conflict.unsaved_item) { + unsavedItem = FilterDisallowedRemotePayloadsAndMap([conflict.unsaved_item])[0] + } + + if (conflict.server_item) { + serverItem = FilterDisallowedRemotePayloadsAndMap([conflict.server_item])[0] + } + + if (!trustedConflicts[conflict.type]) { + trustedConflicts[conflict.type] = [] + } + + const conflictArray = trustedConflicts[conflict.type] + if (conflictArray) { + const entry: TrustedConflictParams = { + type: conflict.type, + server_item: serverItem, + unsaved_item: unsavedItem, + } + conflictArray.push(entry) + } + } + + return trustedConflicts + } + public get error(): HttpError | undefined { return isErrorResponse(this.rawResponse) ? this.rawResponse.data?.error : undefined } @@ -66,56 +117,9 @@ export class ServerSyncResponse { } public get numberOfItemsInvolved(): number { - return this.allFullyFormedPayloads.length - } + const allPayloads = [...this.retrievedPayloads, ...this.rawConflictObjects] - private get allFullyFormedPayloads(): FilteredServerItem[] { - return [ - ...this.retrievedPayloads, - ...this.dataConflictPayloads, - ...this.uuidConflictPayloads, - ...this.rejectedPayloads, - ] - } - - private get rawUuidConflictItems(): ServerItemResponse[] { - return this.rawConflictObjects - .filter((conflict) => { - return conflict.type === ConflictType.UuidConflict - }) - .map((conflict) => { - return conflict.unsaved_item || conflict.item! - }) - } - - private get rawDataConflictItems(): ServerItemResponse[] { - return this.rawConflictObjects - .filter((conflict) => { - return conflict.type === ConflictType.ConflictingData - }) - .map((conflict) => { - return conflict.server_item || conflict.item! - }) - } - - private get rawRejectedPayloads(): ServerItemResponse[] { - return this.rawConflictObjects - .filter((conflict) => { - return ( - conflict.type === ConflictType.ContentTypeError || - conflict.type === ConflictType.ContentError || - conflict.type === ConflictType.ReadOnlyError - ) - }) - .map((conflict) => { - return conflict.unsaved_item! - }) - } - - private get rawConflictObjects(): ConflictParams[] { - const conflicts = this.successResponseData?.conflicts || [] - const legacyConflicts = this.successResponseData?.unsaved || [] - return conflicts.concat(legacyConflicts) + return allPayloads.length } public get hasError(): boolean { diff --git a/packages/snjs/lib/Services/Sync/Account/ResponseResolver.ts b/packages/snjs/lib/Services/Sync/Account/ResponseResolver.ts index f739513cc..646606db3 100644 --- a/packages/snjs/lib/Services/Sync/Account/ResponseResolver.ts +++ b/packages/snjs/lib/Services/Sync/Account/ResponseResolver.ts @@ -1,3 +1,4 @@ +import { ConflictParams, ConflictType } from '@standardnotes/responses' import { ImmutablePayloadCollection, HistoryMap, @@ -11,13 +12,12 @@ import { DeltaRemoteRejected, DeltaEmit, } from '@standardnotes/models' +import { DecryptedServerConflictMap } from './ServerConflictMap' type PayloadSet = { retrievedPayloads: FullyFormedPayloadInterface[] savedPayloads: ServerSyncSavedContextualPayload[] - uuidConflictPayloads: FullyFormedPayloadInterface[] - dataConflictPayloads: FullyFormedPayloadInterface[] - rejectedPayloads: FullyFormedPayloadInterface[] + conflicts: DecryptedServerConflictMap } /** @@ -39,8 +39,8 @@ export class ServerSyncResponseResolver { emits.push(this.processRetrievedPayloads()) emits.push(this.processSavedPayloads()) - emits.push(this.processUuidConflictPayloads()) - emits.push(this.processDataConflictPayloads()) + emits.push(this.processUuidConflictUnsavedPayloads()) + emits.push(this.processDataConflictServerPayloads()) emits.push(this.processRejectedPayloads()) return emits @@ -60,27 +60,42 @@ export class ServerSyncResponseResolver { return delta.result() } - private processDataConflictPayloads(): DeltaEmit { - const collection = ImmutablePayloadCollection.WithPayloads(this.payloadSet.dataConflictPayloads) + private getConflictsForType>(type: ConflictType): T[] { + const results = this.payloadSet.conflicts[type] || [] - const delta = new DeltaRemoteDataConflicts(this.baseCollection, collection, this.historyMap) + return results as T[] + } + + private processDataConflictServerPayloads(): DeltaEmit { + const delta = new DeltaRemoteDataConflicts( + this.baseCollection, + this.getConflictsForType(ConflictType.ConflictingData), + this.historyMap, + ) return delta.result() } - private processUuidConflictPayloads(): DeltaEmit { - const collection = ImmutablePayloadCollection.WithPayloads(this.payloadSet.uuidConflictPayloads) - - const delta = new DeltaRemoteUuidConflicts(this.baseCollection, collection) + private processUuidConflictUnsavedPayloads(): DeltaEmit { + const delta = new DeltaRemoteUuidConflicts(this.baseCollection, this.getConflictsForType(ConflictType.UuidConflict)) return delta.result() } private processRejectedPayloads(): DeltaEmit { - const collection = ImmutablePayloadCollection.WithPayloads(this.payloadSet.rejectedPayloads) + const conflicts = [ + ...this.getConflictsForType(ConflictType.ContentTypeError), + ...this.getConflictsForType(ConflictType.ContentError), + ...this.getConflictsForType(ConflictType.ReadOnlyError), + ...this.getConflictsForType(ConflictType.UuidError), + ...this.getConflictsForType(ConflictType.SharedVaultSnjsVersionError), + ...this.getConflictsForType(ConflictType.SharedVaultInsufficientPermissionsError), + ...this.getConflictsForType(ConflictType.SharedVaultNotMemberError), + ...this.getConflictsForType(ConflictType.SharedVaultInvalidState), + ] - const delta = new DeltaRemoteRejected(this.baseCollection, collection) - - return delta.result() + const delta = new DeltaRemoteRejected(this.baseCollection, conflicts) + const result = delta.result() + return result } } diff --git a/packages/snjs/lib/Services/Sync/Account/ServerConflictMap.ts b/packages/snjs/lib/Services/Sync/Account/ServerConflictMap.ts new file mode 100644 index 000000000..1951bda56 --- /dev/null +++ b/packages/snjs/lib/Services/Sync/Account/ServerConflictMap.ts @@ -0,0 +1,5 @@ +import { ConflictType, ConflictParams } from '@standardnotes/responses' +import { FullyFormedPayloadInterface, TrustedConflictParams } from '@standardnotes/models' + +export type TrustedServerConflictMap = Partial> +export type DecryptedServerConflictMap = Partial[]>> diff --git a/packages/snjs/lib/Services/Sync/SyncService.ts b/packages/snjs/lib/Services/Sync/SyncService.ts index ddcce3fac..9ecb80911 100644 --- a/packages/snjs/lib/Services/Sync/SyncService.ts +++ b/packages/snjs/lib/Services/Sync/SyncService.ts @@ -1,15 +1,15 @@ +import { ConflictParams, ConflictType } from '@standardnotes/responses' import { log, LoggingDomain } from './../../Logging' import { AccountSyncOperation } from '@Lib/Services/Sync/Account/Operation' import { ContentType } from '@standardnotes/common' import { + Uuids, extendArray, isNotUndefined, isNullOrUndefined, removeFromIndex, sleep, subtractFromArray, - useBoolean, - Uuids, } from '@standardnotes/utils' import { ItemManager } from '@Lib/Services/Items/ItemManager' import { OfflineSyncOperation } from '@Lib/Services/Sync/Offline/Operation' @@ -56,6 +56,12 @@ import { getIncrementedDirtyIndex, getCurrentDirtyIndex, ItemContent, + KeySystemItemsKeyContent, + KeySystemItemsKeyInterface, + FullyFormedTransferPayload, + ItemMutator, + isDecryptedOrDeletedItem, + MutationType, } from '@standardnotes/models' import { AbstractService, @@ -71,11 +77,14 @@ import { SyncOptions, SyncQueueStrategy, SyncServiceInterface, - DiagnosticInfo, EncryptionService, DeviceInterface, isFullEntryLoadChunkResponse, isChunkFullEntry, + SyncEventReceivedSharedVaultInvitesData, + SyncEventReceivedRemoteSharedVaultsData, + SyncEventReceivedUserEventsData, + SyncEventReceivedAsymmetricMessagesData, } from '@standardnotes/services' import { OfflineSyncResponse } from './Offline/Response' import { @@ -86,10 +95,23 @@ import { } from '@standardnotes/encryption' import { CreatePayloadFromRawServerItem } from './Account/Utilities' import { ApplicationSyncOptions } from '@Lib/Application/Options/OptionalOptions' +import { DecryptedServerConflictMap, TrustedServerConflictMap } from './Account/ServerConflictMap' const DEFAULT_MAJOR_CHANGE_THRESHOLD = 15 const INVALID_SESSION_RESPONSE_STATUS = 401 +/** Content types appearing first are always mapped first */ +const ContentTypeLocalLoadPriorty = [ + ContentType.ItemsKey, + ContentType.KeySystemRootKey, + ContentType.KeySystemItemsKey, + ContentType.VaultListing, + ContentType.TrustedContact, + ContentType.UserPrefs, + ContentType.Component, + ContentType.Theme, +] + /** * The sync service orchestrates with the model manager, api service, and storage service * to ensure consistent state between the three. When a change is made to an item, consumers @@ -100,7 +122,7 @@ const INVALID_SESSION_RESPONSE_STATUS = 401 * The sync service largely does not perform any task unless it is called upon. */ export class SNSyncService - extends AbstractService + extends AbstractService implements SyncServiceInterface, InternalEventHandlerInterface, SyncClientInterface { private dirtyIndexAtLastPresyncSave?: number @@ -128,14 +150,6 @@ export class SNSyncService public lastSyncInvokationPromise?: Promise public currentSyncRequestPromise?: Promise - /** Content types appearing first are always mapped first */ - private readonly localLoadPriorty = [ - ContentType.ItemsKey, - ContentType.UserPrefs, - ContentType.Component, - ContentType.Theme, - ] - constructor( private itemManager: ItemManager, private sessionManager: SNSessionManager, @@ -225,29 +239,21 @@ export class SNSyncService return this.databaseLoaded } - private async processItemsKeysFirstDuringDatabaseLoad( - itemsKeysPayloads: FullyFormedPayloadInterface[], - ): Promise { - if (itemsKeysPayloads.length === 0) { + private async processPriorityItemsForDatabaseLoad(items: FullyFormedPayloadInterface[]): Promise { + if (items.length === 0) { return } - const encryptedItemsKeysPayloads = itemsKeysPayloads.filter(isEncryptedPayload) + const encryptedPayloads = items.filter(isEncryptedPayload) + const alreadyDecryptedPayloads = items.filter(isDecryptedPayload) as DecryptedPayloadInterface[] - const originallyDecryptedItemsKeysPayloads = itemsKeysPayloads.filter( - isDecryptedPayload, - ) as DecryptedPayloadInterface[] + const encryptionSplit = SplitPayloadsByEncryptionType(encryptedPayloads) + const decryptionSplit = CreateDecryptionSplitWithKeyLookup(encryptionSplit) - const itemsKeysSplit: KeyedDecryptionSplit = { - usesRootKeyWithKeyLookup: { - items: encryptedItemsKeysPayloads, - }, - } - - const newlyDecryptedItemsKeys = await this.protocolService.decryptSplit(itemsKeysSplit) + const newlyDecryptedPayloads = await this.protocolService.decryptSplit(decryptionSplit) await this.payloadManager.emitPayloads( - [...originallyDecryptedItemsKeysPayloads, ...newlyDecryptedItemsKeys], + [...alreadyDecryptedPayloads, ...newlyDecryptedPayloads], PayloadEmitSource.LocalDatabaseLoaded, ) } @@ -262,7 +268,7 @@ export class SNSyncService const chunks = await this.device.getDatabaseLoadChunks( { batchSize: this.options.loadBatchSize, - contentTypePriority: this.localLoadPriorty, + contentTypePriority: ContentTypeLocalLoadPriorty, uuidPriority: this.launchPriorityUuids, }, this.identifier, @@ -272,18 +278,30 @@ export class SNSyncService ? chunks.fullEntries.itemsKeys.entries : await this.device.getDatabaseEntries(this.identifier, chunks.keys.itemsKeys.keys) - const itemsKeyPayloads = itemsKeyEntries - .map((entry) => { - try { - return CreatePayload(entry, PayloadSource.Constructor) - } catch (e) { - console.error('Creating payload failed', e) - return undefined - } - }) - .filter(isNotUndefined) + const keySystemRootKeyEntries = isFullEntryLoadChunkResponse(chunks) + ? chunks.fullEntries.keySystemRootKeys.entries + : await this.device.getDatabaseEntries(this.identifier, chunks.keys.keySystemRootKeys.keys) - await this.processItemsKeysFirstDuringDatabaseLoad(itemsKeyPayloads) + const keySystemItemsKeyEntries = isFullEntryLoadChunkResponse(chunks) + ? chunks.fullEntries.keySystemItemsKeys.entries + : await this.device.getDatabaseEntries(this.identifier, chunks.keys.keySystemItemsKeys.keys) + + const createPayloadFromEntry = (entry: FullyFormedTransferPayload) => { + try { + return CreatePayload(entry, PayloadSource.LocalDatabaseLoaded) + } catch (e) { + console.error('Creating payload failed', e) + return undefined + } + } + + await this.processPriorityItemsForDatabaseLoad(itemsKeyEntries.map(createPayloadFromEntry).filter(isNotUndefined)) + await this.processPriorityItemsForDatabaseLoad( + keySystemRootKeyEntries.map(createPayloadFromEntry).filter(isNotUndefined), + ) + await this.processPriorityItemsForDatabaseLoad( + keySystemItemsKeyEntries.map(createPayloadFromEntry).filter(isNotUndefined), + ) /** * Map in batches to give interface a chance to update. Note that total decryption @@ -308,7 +326,7 @@ export class SNSyncService const payloads = dbEntries .map((entry) => { try { - return CreatePayload(entry, PayloadSource.Constructor) + return CreatePayload(entry, PayloadSource.LocalDatabaseLoaded) } catch (e) { console.error('Creating payload failed', e) return undefined @@ -348,13 +366,10 @@ export class SNSyncService } } - const split: KeyedDecryptionSplit = { - usesItemsKeyWithKeyLookup: { - items: encrypted, - }, - } + const encryptionSplit = SplitPayloadsByEncryptionType(encrypted) + const decryptionSplit = CreateDecryptionSplitWithKeyLookup(encryptionSplit) - const results = await this.protocolService.decryptSplit(split) + const results = await this.protocolService.decryptSplit(decryptionSplit) await this.payloadManager.emitPayloads([...nonencrypted, ...results], PayloadEmitSource.LocalDatabaseLoaded) @@ -616,11 +631,7 @@ export class SNSyncService if (useStrategy === SyncQueueStrategy.ResolveOnNext) { return this.queueStrategyResolveOnNext() } else if (useStrategy === SyncQueueStrategy.ForceSpawnNew) { - return this.queueStrategyForceSpawnNew({ - mode: options.mode, - checkIntegrity: options.checkIntegrity, - source: options.source, - }) + return this.queueStrategyForceSpawnNew(options) } else { throw Error(`Unhandled timing strategy ${useStrategy}`) } @@ -634,7 +645,7 @@ export class SNSyncService ) { this.opStatus.setDidBegin() - await this.notifyEvent(SyncEvent.SyncWillBegin) + await this.notifyEvent(SyncEvent.SyncDidBeginProcessing) /** * Subtract from array as soon as we're sure they'll be called. @@ -647,12 +658,41 @@ export class SNSyncService * Setting this value means the item was 100% sent to the server. */ if (items.length > 0) { - return this.itemManager.setLastSyncBeganForItems(items, beginDate, frozenDirtyIndex) + return this.setLastSyncBeganForItems(items, beginDate, frozenDirtyIndex) } else { return items } } + private async setLastSyncBeganForItems( + itemsToLookupUuidsFor: (DecryptedItemInterface | DeletedItemInterface)[], + date: Date, + globalDirtyIndex: number, + ): Promise<(DecryptedItemInterface | DeletedItemInterface)[]> { + const uuids = Uuids(itemsToLookupUuidsFor) + + const items = this.itemManager.getCollection().findAll(uuids).filter(isDecryptedOrDeletedItem) + + const payloads: (DecryptedPayloadInterface | DeletedPayloadInterface)[] = [] + + for (const item of items) { + const mutator = new ItemMutator( + item, + MutationType.NonDirtying, + ) + + mutator.setBeginSync(date, globalDirtyIndex) + + const payload = mutator.getResult() + + payloads.push(payload) + } + + await this.payloadManager.emitPayloads(payloads, PayloadEmitSource.PreSyncSave) + + return this.itemManager.findAnyItems(uuids) as (DecryptedItemInterface | DeletedItemInterface)[] + } + /** * The InTime resolve queue refers to any sync requests that were made while we still * have not sent out the current request. So, anything in the InTime resolve queue @@ -725,12 +765,15 @@ export class SNSyncService private async createServerSyncOperation( payloads: ServerSyncPushContextualPayload[], - checkIntegrity: boolean, - source: SyncSource, + options: SyncOptions, mode: SyncMode = SyncMode.Default, ) { - const syncToken = await this.getLastSyncToken() - const paginationToken = await this.getPaginationToken() + const syncToken = + options.sharedVaultUuids && options.sharedVaultUuids.length > 0 && options.syncSharedVaultsFromScratch + ? undefined + : await this.getLastSyncToken() + const paginationToken = + options.sharedVaultUuids && options.syncSharedVaultsFromScratch ? undefined : await this.getPaginationToken() const operation = new AccountSyncOperation( payloads, @@ -753,20 +796,23 @@ export class SNSyncService break } }, - syncToken, - paginationToken, this.apiService, + { + syncToken, + paginationToken, + sharedVaultUuids: options.sharedVaultUuids, + }, ) log( LoggingDomain.Sync, 'Syncing online user', 'source', - SyncSource[source], + SyncSource[options.source], 'operation id', operation.id, 'integrity check', - checkIntegrity, + options.checkIntegrity, 'mode', SyncMode[mode], 'syncToken', @@ -789,12 +835,7 @@ export class SNSyncService const { uploadPayloads, syncMode } = await this.getOnlineSyncParameters(payloads, options.mode) return { - operation: await this.createServerSyncOperation( - uploadPayloads, - useBoolean(options.checkIntegrity, false), - options.source, - syncMode, - ), + operation: await this.createServerSyncOperation(uploadPayloads, options, syncMode), mode: syncMode, } } else { @@ -867,6 +908,7 @@ export class SNSyncService await this.notifyEventSync(SyncEvent.SyncCompletedWithAllItemsUploadedAndDownloaded, { source: options.source, + options, }) this.resolvePendingSyncRequestsThatMadeItInTimeOfCurrentRequest(inTimeResolveQueue) @@ -889,7 +931,7 @@ export class SNSyncService this.opStatus.clearError() - await this.notifyEvent(SyncEvent.SingleRoundTripSyncCompleted, response) + await this.notifyEvent(SyncEvent.PaginatedSyncRequestCompleted, response) } private handleErrorServerResponse(response: ServerSyncResponse) { @@ -917,19 +959,36 @@ export class SNSyncService const historyMap = this.historyService.getHistoryMapCopy() + if (response.userEvents) { + await this.notifyEventSync(SyncEvent.ReceivedUserEvents, response.userEvents as SyncEventReceivedUserEventsData) + } + + if (response.asymmetricMessages) { + await this.notifyEventSync( + SyncEvent.ReceivedAsymmetricMessages, + response.asymmetricMessages as SyncEventReceivedAsymmetricMessagesData, + ) + } + + if (response.vaults) { + await this.notifyEventSync( + SyncEvent.ReceivedRemoteSharedVaults, + response.vaults as SyncEventReceivedRemoteSharedVaultsData, + ) + } + + if (response.vaultInvites) { + await this.notifyEventSync( + SyncEvent.ReceivedSharedVaultInvites, + response.vaultInvites as SyncEventReceivedSharedVaultInvitesData, + ) + } + const resolver = new ServerSyncResponseResolver( { retrievedPayloads: await this.processServerPayloads(response.retrievedPayloads, PayloadSource.RemoteRetrieved), savedPayloads: response.savedPayloads, - uuidConflictPayloads: await this.processServerPayloads( - response.uuidConflictPayloads, - PayloadSource.RemoteRetrieved, - ), - dataConflictPayloads: await this.processServerPayloads( - response.dataConflictPayloads, - PayloadSource.RemoteRetrieved, - ), - rejectedPayloads: await this.processServerPayloads(response.rejectedPayloads, PayloadSource.RemoteRetrieved), + conflicts: await this.decryptServerConflicts(response.conflicts), }, masterCollection, operation.payloadsSavedOrSaving, @@ -954,11 +1013,69 @@ export class SNSyncService await this.persistPayloads(payloadsToPersist) } - await Promise.all([ - this.setLastSyncToken(response.lastSyncToken as string), - this.setPaginationToken(response.paginationToken as string), - this.notifyEvent(SyncEvent.SingleRoundTripSyncCompleted, response), - ]) + if (!operation.options.sharedVaultUuids) { + await Promise.all([ + this.setLastSyncToken(response.lastSyncToken as string), + this.setPaginationToken(response.paginationToken as string), + ]) + } + + await this.notifyEvent(SyncEvent.PaginatedSyncRequestCompleted, { + ...response, + uploadedPayloads: operation.payloads, + options: operation.options, + }) + } + + private async decryptServerConflicts(conflictMap: TrustedServerConflictMap): Promise { + const decrypted: DecryptedServerConflictMap = {} + + for (const conflictType of Object.keys(conflictMap)) { + const conflictsForType = conflictMap[conflictType as ConflictType] + if (!conflictsForType) { + continue + } + + if (!decrypted[conflictType as ConflictType]) { + decrypted[conflictType as ConflictType] = [] + } + + const decryptedConflictsForType = decrypted[conflictType as ConflictType] + if (!decryptedConflictsForType) { + throw Error('Decrypted conflicts for type should exist') + } + + for (const conflict of conflictsForType) { + const decryptedUnsavedItem = conflict.unsaved_item + ? await this.processServerPayload(conflict.unsaved_item, PayloadSource.RemoteRetrieved) + : undefined + + const decryptedServerItem = conflict.server_item + ? await this.processServerPayload(conflict.server_item, PayloadSource.RemoteRetrieved) + : undefined + + const decryptedEntry: ConflictParams = < + ConflictParams + >{ + type: conflict.type, + unsaved_item: decryptedUnsavedItem, + server_item: decryptedServerItem, + } + + decryptedConflictsForType.push(decryptedEntry) + } + } + + return decrypted + } + + private async processServerPayload( + item: FilteredServerItem, + source: PayloadSource, + ): Promise { + const result = await this.processServerPayloads([item], source) + + return result[0] } private async processServerPayloads( @@ -971,7 +1088,8 @@ export class SNSyncService const results: FullyFormedPayloadInterface[] = [...deleted] - const { rootKeyEncryption, itemsKeyEncryption } = SplitPayloadsByEncryptionType(encrypted) + const { rootKeyEncryption, itemsKeyEncryption, keySystemRootKeyEncryption } = + SplitPayloadsByEncryptionType(encrypted) const { results: rootKeyDecryptionResults, map: processedItemsKeys } = await this.decryptServerItemsKeys( rootKeyEncryption || [], @@ -979,8 +1097,16 @@ export class SNSyncService extendArray(results, rootKeyDecryptionResults) + const { results: keySystemRootKeyDecryptionResults, map: processedKeySystemItemsKeys } = + await this.decryptServerKeySystemItemsKeys(keySystemRootKeyEncryption || []) + + extendArray(results, keySystemRootKeyDecryptionResults) + if (itemsKeyEncryption) { - const decryptionResults = await this.decryptProcessedServerPayloads(itemsKeyEncryption, processedItemsKeys) + const decryptionResults = await this.decryptProcessedServerPayloads(itemsKeyEncryption, { + ...processedItemsKeys, + ...processedKeySystemItemsKeys, + }) extendArray(results, decryptionResults) } @@ -1017,17 +1143,53 @@ export class SNSyncService } } + private async decryptServerKeySystemItemsKeys(payloads: EncryptedPayloadInterface[]) { + const map: Record> = {} + + if (payloads.length === 0) { + return { + results: [], + map, + } + } + + const keySystemRootKeySplit: KeyedDecryptionSplit = { + usesKeySystemRootKeyWithKeyLookup: { + items: payloads, + }, + } + + const results = await this.protocolService.decryptSplit(keySystemRootKeySplit) + + results.forEach((result) => { + if ( + isDecryptedPayload(result) && + result.content_type === ContentType.KeySystemItemsKey + ) { + map[result.uuid] = result + } + }) + + return { + results, + map, + } + } + private async decryptProcessedServerPayloads( payloads: EncryptedPayloadInterface[], - map: Record>, + map: Record>, ): Promise<(EncryptedPayloadInterface | DecryptedPayloadInterface)[]> { return Promise.all( payloads.map(async (encrypted) => { - const previouslyProcessedItemsKey: DecryptedPayloadInterface | undefined = - map[encrypted.items_key_id as string] + const previouslyProcessedItemsKey: + | DecryptedPayloadInterface + | undefined = map[encrypted.items_key_id as string] const itemsKey = previouslyProcessedItemsKey - ? (CreateDecryptedItemFromPayload(previouslyProcessedItemsKey) as ItemsKeyInterface) + ? (CreateDecryptedItemFromPayload(previouslyProcessedItemsKey) as + | ItemsKeyInterface + | KeySystemItemsKeyInterface) : undefined const keyedSplit: KeyedDecryptionSplit = {} @@ -1251,26 +1413,13 @@ export class SNSyncService await this.persistPayloads(emit.emits) } - override async getDiagnostics(): Promise { - const dirtyUuids = Uuids(this.itemsNeedingSync()) - - return { - sync: { - syncToken: await this.getLastSyncToken(), - cursorToken: await this.getPaginationToken(), - dirtyIndexAtLastPresyncSave: this.dirtyIndexAtLastPresyncSave, - lastSyncDate: this.lastSyncDate, - outOfSync: this.outOfSync, - completedOnlineDownloadFirstSync: this.completedOnlineDownloadFirstSync, - clientLocked: this.clientLocked, - databaseLoaded: this.databaseLoaded, - syncLock: this.syncLock, - dealloced: this.dealloced, - itemsNeedingSync: dirtyUuids, - itemsNeedingSyncCount: dirtyUuids.length, - pendingRequestCount: this.resolveQueue.length + this.spawnQueue.length, - }, - } + async syncSharedVaultsFromScratch(sharedVaultUuids: string[]): Promise { + await this.sync({ + sharedVaultUuids: sharedVaultUuids, + syncSharedVaultsFromScratch: true, + queueStrategy: SyncQueueStrategy.ForceSpawnNew, + awaitAll: true, + }) } /** @e2e_testing */ diff --git a/packages/snjs/lib/Strings/Input.ts b/packages/snjs/lib/Strings/Input.ts deleted file mode 100644 index d1f1082de..000000000 --- a/packages/snjs/lib/Strings/Input.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const InputStrings = { - FileAccountPassword: 'File account password', -} diff --git a/packages/snjs/lib/Strings/index.ts b/packages/snjs/lib/Strings/index.ts index ca0aa6255..7da9fe31d 100644 --- a/packages/snjs/lib/Strings/index.ts +++ b/packages/snjs/lib/Strings/index.ts @@ -1,9 +1,7 @@ import { ConfirmStrings } from './Confirm' -import { InputStrings } from './Input' import { NetworkStrings } from './Network' export const Strings = { Network: NetworkStrings, Confirm: ConfirmStrings, - Input: InputStrings, } diff --git a/packages/snjs/lib/index.ts b/packages/snjs/lib/index.ts index 92d8f264a..ca3479706 100644 --- a/packages/snjs/lib/index.ts +++ b/packages/snjs/lib/index.ts @@ -9,6 +9,7 @@ export * from './Types' export * from './Version' export * from '@standardnotes/common' export * from '@standardnotes/domain-core' +export * from '@standardnotes/api' export * from '@standardnotes/encryption' export * from '@standardnotes/features' export * from '@standardnotes/files' diff --git a/packages/snjs/lib/tsconfig.json b/packages/snjs/lib/tsconfig.json index 5940d3fb3..5b1319423 100644 --- a/packages/snjs/lib/tsconfig.json +++ b/packages/snjs/lib/tsconfig.json @@ -8,6 +8,8 @@ "emitDeclarationOnly": true, "esModuleInterop": true, "isolatedModules": true, + "lib": ["es6", "dom", "es2016", "es2017"], + "module": "esnext", "moduleResolution": "node", "newLine": "lf", "noFallthroughCasesInSwitch": true, @@ -17,11 +19,11 @@ "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true, - "skipLibCheck": true, "outDir": "../dist/@types", + "skipLibCheck": true, "strict": true, "strictNullChecks": true, - "target": "esnext", + "target": "es6", "paths": { "@Lib/*": ["*"], "@Services/*": ["Services/*"] diff --git a/packages/snjs/mocha/000.test.js b/packages/snjs/mocha/000.test.js index 5f9b984c4..f612e88ae 100644 --- a/packages/snjs/mocha/000.test.js +++ b/packages/snjs/mocha/000.test.js @@ -22,7 +22,7 @@ describe('000 legacy protocol operations', () => { let error try { - protocol004.generateDecryptedParametersSync({ + protocol004.generateDecryptedParameters({ uuid: 'foo', content: string, content_type: 'foo', diff --git a/packages/snjs/mocha/004.test.js b/packages/snjs/mocha/004.test.js index 1a99816df..2b08a9041 100644 --- a/packages/snjs/mocha/004.test.js +++ b/packages/snjs/mocha/004.test.js @@ -7,16 +7,16 @@ const expect = chai.expect describe('004 protocol operations', function () { const _identifier = 'hello@test.com' const _password = 'password' - let _keyParams - let _key + let rootKeyParams + let rootKey const application = Factory.createApplicationWithRealCrypto() const protocol004 = new SNProtocolOperator004(new SNWebCrypto()) before(async function () { await Factory.initializeApplication(application) - _key = await protocol004.createRootKey(_identifier, _password, KeyParamsOrigination.Registration) - _keyParams = _key.keyParams + rootKey = await protocol004.createRootKey(_identifier, _password, KeyParamsOrigination.Registration) + rootKeyParams = rootKey.keyParams }) after(async function () { @@ -69,43 +69,58 @@ describe('004 protocol operations', function () { }) it('properly encrypts and decrypts', async function () { - const text = 'hello world' - const rawKey = _key.masterKey - const nonce = await application.protocolService.crypto.generateRandomKey(192) + const payload = new DecryptedPayload({ + uuid: '123', + content_type: ContentType.ItemsKey, + content: FillItemContent({ + title: 'foo', + text: 'bar', + }), + }) + const operator = application.protocolService.operatorManager.operatorForVersion(ProtocolVersion.V004) - const authenticatedData = { foo: 'bar' } - const encString = await operator.encryptString004(text, rawKey, nonce, authenticatedData) - const decString = await operator.decryptString004( - encString, - rawKey, - nonce, - await operator.authenticatedDataToString(authenticatedData), - ) - expect(decString).to.equal(text) + + const encrypted = await operator.generateEncryptedParameters(payload, rootKey) + const decrypted = await operator.generateDecryptedParameters(encrypted, rootKey) + + expect(decrypted.content.title).to.equal('foo') + expect(decrypted.content.text).to.equal('bar') }) it('fails to decrypt non-matching aad', async function () { - const text = 'hello world' - const rawKey = _key.masterKey - const nonce = await application.protocolService.crypto.generateRandomKey(192) + const payload = new DecryptedPayload({ + uuid: '123', + content_type: ContentType.ItemsKey, + content: FillItemContent({ + title: 'foo', + text: 'bar', + }), + }) + const operator = application.protocolService.operatorManager.operatorForVersion(ProtocolVersion.V004) - const aad = { foo: 'bar' } - const nonmatchingAad = { foo: 'rab' } - const encString = await operator.encryptString004(text, rawKey, nonce, aad) - const decString = await operator.decryptString004(encString, rawKey, nonce, nonmatchingAad) - expect(decString).to.not.be.ok + + const encrypted = await operator.generateEncryptedParameters(payload, rootKey) + const decrypted = await operator.generateDecryptedParameters( + { + ...encrypted, + uuid: 'nonmatching', + }, + rootKey, + ) + + expect(decrypted.errorDecrypting).to.equal(true) }) it('generates existing keys for key params', async function () { - const key = await application.protocolService.computeRootKey(_password, _keyParams) - expect(key.compare(_key)).to.be.true + const key = await application.protocolService.computeRootKey(_password, rootKeyParams) + expect(key.compare(rootKey)).to.be.true }) it('can decrypt encrypted params', async function () { const payload = Factory.createNotePayload() const key = await protocol004.createItemsKey() - const params = await protocol004.generateEncryptedParametersSync(payload, key) - const decrypted = await protocol004.generateDecryptedParametersSync(params, key) + const params = await protocol004.generateEncryptedParameters(payload, key) + const decrypted = await protocol004.generateDecryptedParameters(params, key) expect(decrypted.errorDecrypting).to.not.be.ok expect(decrypted.content).to.eql(payload.content) }) @@ -113,9 +128,9 @@ describe('004 protocol operations', function () { it('modifying the uuid of the payload should fail to decrypt', async function () { const payload = Factory.createNotePayload() const key = await protocol004.createItemsKey() - const params = await protocol004.generateEncryptedParametersSync(payload, key) + const params = await protocol004.generateEncryptedParameters(payload, key) params.uuid = 'foo' - const result = await protocol004.generateDecryptedParametersSync(params, key) + const result = await protocol004.generateDecryptedParameters(params, key) expect(result.errorDecrypting).to.equal(true) }) }) diff --git a/packages/snjs/mocha/TestRegistry/BaseTests.js b/packages/snjs/mocha/TestRegistry/BaseTests.js new file mode 100644 index 000000000..da672bf47 --- /dev/null +++ b/packages/snjs/mocha/TestRegistry/BaseTests.js @@ -0,0 +1,58 @@ +export const BaseTests = [ + 'memory.test.js', + 'protocol.test.js', + 'utils.test.js', + '000.test.js', + '001.test.js', + '002.test.js', + '003.test.js', + '004.test.js', + 'username.test.js', + 'app-group.test.js', + 'application.test.js', + 'payload.test.js', + 'payload_encryption.test.js', + 'item.test.js', + 'item_manager.test.js', + 'features.test.js', + 'settings.test.js', + 'mfa_service.test.js', + 'mutator.test.js', + 'mutator_service.test.js', + 'payload_manager.test.js', + 'collections.test.js', + 'note_display_criteria.test.js', + 'keys.test.js', + 'key_params.test.js', + 'key_recovery_service.test.js', + 'backups.test.js', + 'upgrading.test.js', + 'model_tests/importing.test.js', + 'model_tests/appmodels.test.js', + 'model_tests/items.test.js', + 'model_tests/mapping.test.js', + 'model_tests/notes_smart_tags.test.js', + 'model_tests/notes_tags.test.js', + 'model_tests/notes_tags_folders.test.js', + 'model_tests/performance.test.js', + 'sync_tests/offline.test.js', + 'sync_tests/notes_tags.test.js', + 'sync_tests/online.test.js', + 'sync_tests/conflicting.test.js', + 'sync_tests/integrity.test.js', + 'auth-fringe-cases.test.js', + 'auth.test.js', + 'device_auth.test.js', + 'storage.test.js', + 'protection.test.js', + 'singletons.test.js', + 'migrations/migration.test.js', + 'migrations/tags-to-folders.test.js', + 'history.test.js', + 'actions.test.js', + 'preferences.test.js', + 'files.test.js', + 'session.test.js', + 'subscriptions.test.js', + 'recovery.test.js', +]; diff --git a/packages/snjs/mocha/TestRegistry/MainRegistry.js b/packages/snjs/mocha/TestRegistry/MainRegistry.js new file mode 100644 index 000000000..a93e318a5 --- /dev/null +++ b/packages/snjs/mocha/TestRegistry/MainRegistry.js @@ -0,0 +1,7 @@ +import { BaseTests } from './BaseTests.js' +import { VaultTests } from './VaultTests.js' + +export default { + BaseTests, + VaultTests, +} diff --git a/packages/snjs/mocha/TestRegistry/VaultTests.js b/packages/snjs/mocha/TestRegistry/VaultTests.js new file mode 100644 index 000000000..0f5a5034a --- /dev/null +++ b/packages/snjs/mocha/TestRegistry/VaultTests.js @@ -0,0 +1,16 @@ + +export const VaultTests = [ + 'vaults/vaults.test.js', + 'vaults/pkc.test.js', + 'vaults/contacts.test.js', + 'vaults/crypto.test.js', + 'vaults/asymmetric-messages.test.js', + 'vaults/shared_vaults.test.js', + 'vaults/invites.test.js', + 'vaults/items.test.js', + 'vaults/conflicts.test.js', + 'vaults/deletion.test.js', + 'vaults/permissions.test.js', + 'vaults/key_rotation.test.js', + 'vaults/files.test.js', +]; diff --git a/packages/snjs/mocha/actions.test.js b/packages/snjs/mocha/actions.test.js index 8e6350c52..d28f026b3 100644 --- a/packages/snjs/mocha/actions.test.js +++ b/packages/snjs/mocha/actions.test.js @@ -170,10 +170,7 @@ describe('actions service', () => { }) // Extension item - const extensionItem = await this.application.itemManager.createItem( - ContentType.ActionsExtension, - this.actionsExtension, - ) + const extensionItem = await this.application.mutator.createItem(ContentType.ActionsExtension, this.actionsExtension) this.extensionItemUuid = extensionItem.uuid }) @@ -185,7 +182,7 @@ describe('actions service', () => { }) it('should get extension items', async function () { - await this.itemManager.createItem(ContentType.Note, { + await this.application.mutator.createItem(ContentType.Note, { title: 'A simple note', text: 'Standard Notes rocks! lml.', }) @@ -194,7 +191,7 @@ describe('actions service', () => { }) it('should get extensions in context of item', async function () { - const noteItem = await this.itemManager.createItem(ContentType.Note, { + const noteItem = await this.application.mutator.createItem(ContentType.Note, { title: 'Another note', text: 'Whiskey In The Jar', }) @@ -205,7 +202,7 @@ describe('actions service', () => { }) it('should get actions based on item context', async function () { - const tagItem = await this.itemManager.createItem(ContentType.Tag, { + const tagItem = await this.application.mutator.createItem(ContentType.Tag, { title: 'Music', }) @@ -217,7 +214,7 @@ describe('actions service', () => { }) it('should load extension in context of item', async function () { - const noteItem = await this.itemManager.createItem(ContentType.Note, { + const noteItem = await this.application.mutator.createItem(ContentType.Note, { title: 'Yet another note', text: 'And all things will end ♫', }) @@ -249,7 +246,7 @@ describe('actions service', () => { const sandbox = sinon.createSandbox() before(async function () { - this.noteItem = await this.itemManager.createItem(ContentType.Note, { + this.noteItem = await this.application.mutator.createItem(ContentType.Note, { title: 'Hey', text: 'Welcome To Paradise', }) @@ -331,7 +328,7 @@ describe('actions service', () => { const sandbox = sinon.createSandbox() before(async function () { - this.noteItem = await this.itemManager.createItem(ContentType.Note, { + this.noteItem = await this.application.mutator.createItem(ContentType.Note, { title: 'Excuse Me', text: 'Time To Be King 8)', }) diff --git a/packages/snjs/mocha/application.test.js b/packages/snjs/mocha/application.test.js index 16c468df8..5dc740f42 100644 --- a/packages/snjs/mocha/application.test.js +++ b/packages/snjs/mocha/application.test.js @@ -1,6 +1,6 @@ /* eslint-disable no-unused-expressions */ /* eslint-disable no-undef */ -import { BaseItemCounts } from './lib/Applications.js' +import { BaseItemCounts } from './lib/BaseItemCounts.js' import * as Factory from './lib/factory.js' chai.use(chaiAsPromised) const expect = chai.expect @@ -75,7 +75,7 @@ describe('application instances', () => { /** Recreate app with different host */ const recreatedContext = await Factory.createAppContext({ identifier: 'app', - host: 'http://nonsense.host' + host: 'http://nonsense.host', }) await recreatedContext.launch() @@ -134,7 +134,7 @@ describe('application instances', () => { }) it('shows confirmation dialog when there are unsaved changes', async () => { - await testSNApp.itemManager.setItemDirty(testNote1) + await testSNApp.mutator.setItemDirty(testNote1) await testSNApp.user.signOut() const expectedConfirmMessage = signOutConfirmMessage(1) @@ -154,7 +154,7 @@ describe('application instances', () => { }) it('does not show confirmation dialog when there are unsaved changes and the "force" option is set to true', async () => { - await testSNApp.itemManager.setItemDirty(testNote1) + await testSNApp.mutator.setItemDirty(testNote1) await testSNApp.user.signOut(true) expect(confirmAlert.callCount).to.equal(0) @@ -166,7 +166,7 @@ describe('application instances', () => { confirmAlert.restore() confirmAlert = sinon.stub(testSNApp.alertService, 'confirm').callsFake((_message) => false) - await testSNApp.itemManager.setItemDirty(testNote1) + await testSNApp.mutator.setItemDirty(testNote1) await testSNApp.user.signOut() const expectedConfirmMessage = signOutConfirmMessage(1) diff --git a/packages/snjs/mocha/auth-fringe-cases.test.js b/packages/snjs/mocha/auth-fringe-cases.test.js index 820e3f69e..e15f0c929 100644 --- a/packages/snjs/mocha/auth-fringe-cases.test.js +++ b/packages/snjs/mocha/auth-fringe-cases.test.js @@ -1,6 +1,6 @@ /* eslint-disable no-unused-expressions */ /* eslint-disable no-undef */ -import { BaseItemCounts } from './lib/Applications.js' +import { BaseItemCounts } from './lib/BaseItemCounts.js' import * as Factory from './lib/factory.js' chai.use(chaiAsPromised) const expect = chai.expect @@ -85,14 +85,14 @@ describe('auth fringe cases', () => { const serverText = 'server text' - await context.application.mutator.changeAndSaveItem(firstVersionOfNote, (mutator) => { + await context.application.changeAndSaveItem(firstVersionOfNote, (mutator) => { mutator.text = serverText }) const newApplication = await Factory.signOutApplicationAndReturnNew(context.application) /** Create same note but now offline */ - await newApplication.itemManager.emitItemFromPayload(firstVersionOfNote.payload) + await newApplication.mutator.emitItemFromPayload(firstVersionOfNote.payload) /** Sign in and merge local data */ await newApplication.signIn(context.email, context.password, undefined, undefined, true, true) diff --git a/packages/snjs/mocha/auth.test.js b/packages/snjs/mocha/auth.test.js index d120e491b..24e2a2fde 100644 --- a/packages/snjs/mocha/auth.test.js +++ b/packages/snjs/mocha/auth.test.js @@ -1,6 +1,6 @@ /* eslint-disable no-unused-expressions */ /* eslint-disable no-undef */ -import { BaseItemCounts } from './lib/Applications.js' +import { BaseItemCounts } from './lib/BaseItemCounts.js' import * as Factory from './lib/factory.js' chai.use(chaiAsPromised) const expect = chai.expect @@ -15,7 +15,7 @@ describe('basic auth', function () { beforeEach(async function () { localStorage.clear() - this.expectedItemCount = BaseItemCounts.DefaultItems + this.expectedItemCount = BaseItemCounts.DefaultItemsWithAccount this.context = await Factory.createAppContext() await this.context.launch() this.application = this.context.application @@ -262,7 +262,7 @@ describe('basic auth', function () { if (!didCompleteDownloadFirstSync) { return } - if (!didCompletePostDownloadFirstSync && eventName === SyncEvent.SingleRoundTripSyncCompleted) { + if (!didCompletePostDownloadFirstSync && eventName === SyncEvent.PaginatedSyncRequestCompleted) { didCompletePostDownloadFirstSync = true /** Should be in sync */ outOfSync = this.application.syncService.isOutOfSync() diff --git a/packages/snjs/mocha/backups.test.js b/packages/snjs/mocha/backups.test.js index a6320370a..4fbc406d7 100644 --- a/packages/snjs/mocha/backups.test.js +++ b/packages/snjs/mocha/backups.test.js @@ -1,6 +1,6 @@ /* eslint-disable no-unused-expressions */ /* eslint-disable no-undef */ -import { BaseItemCounts } from './lib/Applications.js' +import { BaseItemCounts } from './lib/BaseItemCounts.js' import * as Factory from './lib/factory.js' chai.use(chaiAsPromised) const expect = chai.expect @@ -25,9 +25,6 @@ describe('backups', function () { this.application = null }) - const BASE_ITEM_COUNT_ENCRYPTED = BaseItemCounts.DefaultItems - const BASE_ITEM_COUNT_DECRYPTED = ['UserPreferences', 'DarkTheme'].length - it('backup file should have a version number', async function () { let data = await this.application.createDecryptedBackupFile() expect(data.version).to.equal(this.application.protocolService.getLatestVersion()) @@ -39,7 +36,9 @@ describe('backups', function () { it('no passcode + no account backup file should have correct number of items', async function () { await Promise.all([Factory.createSyncedNote(this.application), Factory.createSyncedNote(this.application)]) const data = await this.application.createDecryptedBackupFile() - expect(data.items.length).to.equal(BASE_ITEM_COUNT_DECRYPTED + 2) + const offsetForNewItems = 2 + const offsetForNoItemsKey = -1 + expect(data.items.length).to.equal(BaseItemCounts.DefaultItems + offsetForNewItems + offsetForNoItemsKey) }) it('passcode + no account backup file should have correct number of items', async function () { @@ -49,12 +48,12 @@ describe('backups', function () { // Encrypted backup without authorization const encryptedData = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups() - expect(encryptedData.items.length).to.equal(BASE_ITEM_COUNT_ENCRYPTED + 2) + expect(encryptedData.items.length).to.equal(BaseItemCounts.DefaultItems + 2) // Encrypted backup with authorization Factory.handlePasswordChallenges(this.application, passcode) const authorizedEncryptedData = await this.application.createEncryptedBackupFile() - expect(authorizedEncryptedData.items.length).to.equal(BASE_ITEM_COUNT_ENCRYPTED + 2) + expect(authorizedEncryptedData.items.length).to.equal(BaseItemCounts.DefaultItems + 2) }) it('no passcode + account backup file should have correct number of items', async function () { @@ -68,17 +67,17 @@ describe('backups', function () { // Encrypted backup without authorization const encryptedData = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups() - expect(encryptedData.items.length).to.equal(BASE_ITEM_COUNT_ENCRYPTED + 2) + expect(encryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccount + 2) Factory.handlePasswordChallenges(this.application, this.password) // Decrypted backup const decryptedData = await this.application.createDecryptedBackupFile() - expect(decryptedData.items.length).to.equal(BASE_ITEM_COUNT_DECRYPTED + 2) + expect(decryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccountWithoutItemsKey + 2) // Encrypted backup with authorization const authorizedEncryptedData = await this.application.createEncryptedBackupFile() - expect(authorizedEncryptedData.items.length).to.equal(BASE_ITEM_COUNT_ENCRYPTED + 2) + expect(authorizedEncryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccount + 2) }) it('passcode + account backup file should have correct number of items', async function () { @@ -91,17 +90,17 @@ describe('backups', function () { // Encrypted backup without authorization const encryptedData = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups() - expect(encryptedData.items.length).to.equal(BASE_ITEM_COUNT_ENCRYPTED + 2) + expect(encryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccount + 2) Factory.handlePasswordChallenges(this.application, passcode) // Decrypted backup const decryptedData = await this.application.createDecryptedBackupFile() - expect(decryptedData.items.length).to.equal(BASE_ITEM_COUNT_DECRYPTED + 2) + expect(decryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccountWithoutItemsKey + 2) // Encrypted backup with authorization const authorizedEncryptedData = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups() - expect(authorizedEncryptedData.items.length).to.equal(BASE_ITEM_COUNT_ENCRYPTED + 2) + expect(authorizedEncryptedData.items.length).to.equal(BaseItemCounts.DefaultItemsWithAccount + 2) }) it('backup file item should have correct fields', async function () { @@ -154,7 +153,7 @@ describe('backups', function () { errorDecrypting: true, }) - await this.application.itemManager.emitItemFromPayload(errored) + await this.application.payloadManager.emitPayload(errored) const erroredItem = this.application.itemManager.findAnyItem(errored.uuid) @@ -162,7 +161,7 @@ describe('backups', function () { const backupData = await this.application.createDecryptedBackupFile() - expect(backupData.items.length).to.equal(BASE_ITEM_COUNT_DECRYPTED + 2) + expect(backupData.items.length).to.equal(BaseItemCounts.DefaultItemsNoAccounNoItemsKey + 2) }) it('decrypted backup file should not have keyParams', async function () { diff --git a/packages/snjs/mocha/features.test.js b/packages/snjs/mocha/features.test.js index 7d438a5b3..5442950a1 100644 --- a/packages/snjs/mocha/features.test.js +++ b/packages/snjs/mocha/features.test.js @@ -31,9 +31,9 @@ describe('features', () => { expires_at: tomorrow, } - sinon.spy(application.itemManager, 'createItem') - sinon.spy(application.itemManager, 'changeComponent') - sinon.spy(application.itemManager, 'setItemsToBeDeleted') + sinon.spy(application.mutator, 'createItem') + sinon.spy(application.mutator, 'changeComponent') + sinon.spy(application.mutator, 'setItemsToBeDeleted') getUserFeatures = sinon.stub(application.apiService, 'getUserFeatures').callsFake(() => { return Promise.resolve({ @@ -82,7 +82,7 @@ describe('features', () => { it('should fetch user features and create items for features with content type', async () => { expect(application.apiService.getUserFeatures.callCount).to.equal(1) - expect(application.itemManager.createItem.callCount).to.equal(2) + expect(application.mutator.createItem.callCount).to.equal(2) const themeItems = application.items.getItems(ContentType.Theme) const systemThemeCount = 1 @@ -117,7 +117,7 @@ describe('features', () => { // Wipe roles from initial sync await application.featuresService.setOnlineRoles([]) // Create pre-existing item for theme without all the info - await application.itemManager.createItem( + await application.mutator.createItem( ContentType.Theme, FillItemContent({ package_info: { @@ -129,7 +129,7 @@ describe('features', () => { await application.sync.sync() // Timeout since we don't await for features update await new Promise((resolve) => setTimeout(resolve, 1000)) - expect(application.itemManager.changeComponent.callCount).to.equal(1) + expect(application.mutator.changeComponent.callCount).to.equal(1) const themeItems = application.items.getItems(ContentType.Theme) expect(themeItems).to.have.lengthOf(1) expect(themeItems[0].content).to.containSubset( @@ -172,7 +172,7 @@ describe('features', () => { // Timeout since we don't await for features update await new Promise((resolve) => setTimeout(resolve, 1000)) - expect(application.itemManager.setItemsToBeDeleted.calledWith([sinon.match({ uuid: themeItem.uuid })])).to.equal( + expect(application.mutator.setItemsToBeDeleted.calledWith([sinon.match({ uuid: themeItem.uuid })])).to.equal( true, ) @@ -202,7 +202,7 @@ describe('features', () => { sinon.stub(application.featuresService, 'migrateFeatureRepoToUserSetting').callsFake(resolve) }) - await application.itemManager.createItem( + await application.mutator.createItem( ContentType.ExtensionRepo, FillItemContent({ url: `https://extensions.standardnotes.org/${extensionKey}`, @@ -224,7 +224,7 @@ describe('features', () => { // eslint-disable-next-line @typescript-eslint/no-empty-function .callsFake(() => {}) const extensionKey = UuidGenerator.GenerateUuid().split('-').join('') - await application.itemManager.createItem( + await application.mutator.createItem( ContentType.ExtensionRepo, FillItemContent({ url: `https://extensions.standardnotes.org/${extensionKey}`, @@ -255,7 +255,7 @@ describe('features', () => { return false }) const extensionKey = UuidGenerator.GenerateUuid().split('-').join('') - await application.itemManager.createItem( + await application.mutator.createItem( ContentType.ExtensionRepo, FillItemContent({ url: `https://extensions.standardnotes.org/${extensionKey}`, @@ -290,7 +290,7 @@ describe('features', () => { } }) }) - await application.itemManager.createItem( + await application.mutator.createItem( ContentType.ExtensionRepo, FillItemContent({ url: `https://extensions.standardnotes.org/${extensionKey}`, @@ -304,7 +304,7 @@ describe('features', () => { it('previous extension repo should be migrated to offline feature repo', async () => { application = await Factory.signOutApplicationAndReturnNew(application) const extensionKey = UuidGenerator.GenerateUuid().split('-').join('') - await application.itemManager.createItem( + await application.mutator.createItem( ContentType.ExtensionRepo, FillItemContent({ url: `https://extensions.standardnotes.org/${extensionKey}`, diff --git a/packages/snjs/mocha/files.test.js b/packages/snjs/mocha/files.test.js index 2e2e2390e..622453dc9 100644 --- a/packages/snjs/mocha/files.test.js +++ b/packages/snjs/mocha/files.test.js @@ -1,4 +1,5 @@ import * as Factory from './lib/factory.js' +import * as Events from './lib/Events.js' import * as Utils from './lib/Utils.js' import * as Files from './lib/Files.js' @@ -38,22 +39,7 @@ describe('files', function () { }) if (subscription) { - await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', { - userEmail: context.email, - subscriptionId: subscriptionId++, - subscriptionName: 'PRO_PLAN', - subscriptionExpiresAt: (new Date().getTime() + 3_600_000) * 1_000, - timestamp: Date.now(), - offline: false, - discountCode: null, - limitedDiscountPurchased: false, - newSubscriber: true, - totalActiveSubscriptionsCount: 1, - userRegisteredAt: 1, - billingFrequency: 12, - payAmount: 59.00 - }) - await Factory.sleep(2) + await context.publicMockSubscriptionPurchaseEvent() } } @@ -66,7 +52,7 @@ describe('files', function () { await setup({ fakeCrypto: true, subscription: true }) const remoteIdentifier = Utils.generateUuid() - const token = await application.apiService.createFileValetToken(remoteIdentifier, 'write') + const token = await application.apiService.createUserFileValetToken(remoteIdentifier, 'write') expect(token.length).to.be.above(0) }) @@ -75,15 +61,15 @@ describe('files', function () { await setup({ fakeCrypto: true, subscription: false }) const remoteIdentifier = Utils.generateUuid() - const tokenOrError = await application.apiService.createFileValetToken(remoteIdentifier, 'write') + const tokenOrError = await application.apiService.createUserFileValetToken(remoteIdentifier, 'write') - expect(tokenOrError.tag).to.equal('no-subscription') + expect(isClientDisplayableError(tokenOrError)).to.equal(true) }) it('should not create valet token from server when user has an expired subscription - @paidfeature', async function () { await setup({ fakeCrypto: true, subscription: false }) - await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', { + await Events.publishMockedEvent('SUBSCRIPTION_PURCHASED', { userEmail: context.email, subscriptionId: subscriptionId++, subscriptionName: 'PLUS_PLAN', @@ -96,27 +82,27 @@ describe('files', function () { totalActiveSubscriptionsCount: 1, userRegisteredAt: 1, billingFrequency: 12, - payAmount: 59.00 + payAmount: 59.0, }) await Factory.sleep(2) const remoteIdentifier = Utils.generateUuid() - const tokenOrError = await application.apiService.createFileValetToken(remoteIdentifier, 'write') + const tokenOrError = await application.apiService.createUserFileValetToken(remoteIdentifier, 'write') - expect(tokenOrError.tag).to.equal('expired-subscription') + expect(isClientDisplayableError(tokenOrError)).to.equal(true) }) it('creating two upload sessions successively should succeed - @paidfeature', async function () { await setup({ fakeCrypto: true, subscription: true }) - const firstToken = await application.apiService.createFileValetToken(Utils.generateUuid(), 'write') - const firstSession = await application.apiService.startUploadSession(firstToken) + const firstToken = await application.apiService.createUserFileValetToken(Utils.generateUuid(), 'write') + const firstSession = await application.apiService.startUploadSession(firstToken, 'user') expect(firstSession.uploadId).to.be.ok - const secondToken = await application.apiService.createFileValetToken(Utils.generateUuid(), 'write') - const secondSession = await application.apiService.startUploadSession(secondToken) + const secondToken = await application.apiService.createUserFileValetToken(Utils.generateUuid(), 'write') + const secondSession = await application.apiService.startUploadSession(secondToken, 'user') expect(secondSession.uploadId).to.be.ok }) @@ -129,7 +115,7 @@ describe('files', function () { const file = await Files.uploadFile(fileService, buffer, 'my-file', 'md', 1000) - const downloadedBytes = await Files.downloadFile(fileService, itemManager, file.remoteIdentifier) + const downloadedBytes = await Files.downloadFile(fileService, file) expect(downloadedBytes).to.eql(buffer) }) @@ -142,7 +128,7 @@ describe('files', function () { const file = await Files.uploadFile(fileService, buffer, 'my-file', 'md', 100000) - const downloadedBytes = await Files.downloadFile(fileService, itemManager, file.remoteIdentifier) + const downloadedBytes = await Files.downloadFile(fileService, file) expect(downloadedBytes).to.eql(buffer) }) diff --git a/packages/snjs/mocha/history.test.js b/packages/snjs/mocha/history.test.js index a373ea95f..8d189de2c 100644 --- a/packages/snjs/mocha/history.test.js +++ b/packages/snjs/mocha/history.test.js @@ -35,7 +35,7 @@ describe('history manager', () => { }) function setTextAndSync(application, item, text) { - return application.mutator.changeAndSaveItem( + return application.changeAndSaveItem( item, (mutator) => { mutator.text = text @@ -59,7 +59,7 @@ describe('history manager', () => { expect(this.historyManager.sessionHistoryForItem(item).length).to.equal(0) /** Sync with different contents, should create new entry */ - await this.application.mutator.changeAndSaveItem( + await this.application.changeAndSaveItem( item, (mutator) => { mutator.title = Math.random() @@ -79,7 +79,7 @@ describe('history manager', () => { const context = await Factory.createAppContext({ identifier }) await context.launch() expect(context.application.historyManager.sessionHistoryForItem(item).length).to.equal(0) - await context.application.mutator.changeAndSaveItem( + await context.application.changeAndSaveItem( item, (mutator) => { mutator.title = Math.random() @@ -97,13 +97,13 @@ describe('history manager', () => { it('creating new item and making 1 change should create 0 revisions', async function () { const context = await Factory.createAppContext() await context.launch() - const item = await context.application.mutator.createTemplateItem(ContentType.Note, { + const item = await context.application.items.createTemplateItem(ContentType.Note, { references: [], }) await context.application.mutator.insertItem(item) expect(context.application.historyManager.sessionHistoryForItem(item).length).to.equal(0) - await context.application.mutator.changeAndSaveItem( + await context.application.changeAndSaveItem( item, (mutator) => { mutator.title = Math.random() @@ -172,8 +172,8 @@ describe('history manager', () => { text: Factory.randomString(100), }), ) - let item = await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) - await this.application.itemManager.setItemDirty(item) + let item = await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) + await this.application.mutator.setItemDirty(item) await this.application.syncService.sync(syncOptions) /** It should keep the first and last by default */ item = await setTextAndSync(this.application, item, item.content.text) @@ -202,9 +202,9 @@ describe('history manager', () => { }), ) - let item = await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) + let item = await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) - await this.application.itemManager.setItemDirty(item) + await this.application.mutator.setItemDirty(item) await this.application.syncService.sync(syncOptions) item = await setTextAndSync(this.application, item, item.content.text + Factory.randomString(1)) @@ -241,9 +241,9 @@ describe('history manager', () => { it('unsynced entries should use payload created_at for preview titles', async function () { const payload = Factory.createNotePayload() - await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) const item = this.application.items.findItem(payload.uuid) - await this.application.mutator.changeAndSaveItem( + await this.application.changeAndSaveItem( item, (mutator) => { mutator.title = Math.random() @@ -306,7 +306,7 @@ describe('history manager', () => { expect(itemHistory.length).to.equal(1) /** Sync with different contents, should not create a new entry */ - await this.application.mutator.changeAndSaveItem( + await this.application.changeAndSaveItem( item, (mutator) => { mutator.title = Math.random() @@ -327,7 +327,7 @@ describe('history manager', () => { await Factory.sleep(Factory.ServerRevisionFrequency) /** Sync with different contents, should create new entry */ const newTitleAfterFirstChange = `The title should be: ${Math.random()}` - await this.application.mutator.changeAndSaveItem( + await this.application.changeAndSaveItem( item, (mutator) => { mutator.title = newTitleAfterFirstChange @@ -343,7 +343,10 @@ describe('history manager', () => { expect(itemHistory.length).to.equal(2) const oldestEntry = lastElement(itemHistory) - let revisionFromServerOrError = await this.application.getRevision.execute({ itemUuid: item.uuid, revisionUuid: oldestEntry.uuid }) + let revisionFromServerOrError = await this.application.getRevision.execute({ + itemUuid: item.uuid, + revisionUuid: oldestEntry.uuid, + }) const revisionFromServer = revisionFromServerOrError.getValue() expect(revisionFromServer).to.be.ok @@ -359,7 +362,7 @@ describe('history manager', () => { it('duplicate revisions should not have the originals uuid', async function () { const note = await Factory.createSyncedNote(this.application) await Factory.markDirtyAndSyncItem(this.application, note) - const dupe = await this.application.itemManager.duplicateItem(note, true) + const dupe = await this.application.mutator.duplicateItem(note, true) await Factory.markDirtyAndSyncItem(this.application, dupe) await Factory.sleep(Factory.ServerRevisionCreationDelay) @@ -367,7 +370,10 @@ describe('history manager', () => { const dupeHistoryOrError = await this.application.listRevisions.execute({ itemUuid: dupe.uuid }) const dupeHistory = dupeHistoryOrError.getValue() - const dupeRevisionOrError = await this.application.getRevision.execute({ itemUuid: dupe.uuid, revisionUuid: dupeHistory[0].uuid }) + const dupeRevisionOrError = await this.application.getRevision.execute({ + itemUuid: dupe.uuid, + revisionUuid: dupeHistory[0].uuid, + }) const dupeRevision = dupeRevisionOrError.getValue() expect(dupeRevision.payload.uuid).to.equal(dupe.uuid) }) @@ -384,7 +390,7 @@ describe('history manager', () => { await Factory.sleep(Factory.ServerRevisionFrequency) await Factory.markDirtyAndSyncItem(this.application, note) - const dupe = await this.application.itemManager.duplicateItem(note, true) + const dupe = await this.application.mutator.duplicateItem(note, true) await Factory.markDirtyAndSyncItem(this.application, dupe) await Factory.sleep(Factory.ServerRevisionCreationDelay) @@ -405,12 +411,12 @@ describe('history manager', () => { await Factory.sleep(Factory.ServerRevisionFrequency) const changedText = `${Math.random()}` - await this.application.mutator.changeAndSaveItem(note, (mutator) => { + await this.application.changeAndSaveItem(note, (mutator) => { mutator.title = changedText }) await Factory.markDirtyAndSyncItem(this.application, note) - const dupe = await this.application.itemManager.duplicateItem(note, true) + const dupe = await this.application.mutator.duplicateItem(note, true) await Factory.markDirtyAndSyncItem(this.application, dupe) await Factory.sleep(Factory.ServerRevisionCreationDelay) @@ -420,7 +426,10 @@ describe('history manager', () => { expect(itemHistory.length).to.be.above(1) const newestRevision = itemHistory[0] - const fetchedOrError = await this.application.getRevision.execute({ itemUuid: dupe.uuid, revisionUuid: newestRevision.uuid }) + const fetchedOrError = await this.application.getRevision.execute({ + itemUuid: dupe.uuid, + revisionUuid: newestRevision.uuid, + }) const fetched = fetchedOrError.getValue() expect(fetched.payload.errorDecrypting).to.not.be.ok expect(fetched.payload.content.title).to.equal(changedText) diff --git a/packages/snjs/mocha/item_manager.test.js b/packages/snjs/mocha/item_manager.test.js index 6f23c2459..c4c6ed8dd 100644 --- a/packages/snjs/mocha/item_manager.test.js +++ b/packages/snjs/mocha/item_manager.test.js @@ -1,167 +1,120 @@ /* eslint-disable no-unused-expressions */ /* eslint-disable no-undef */ import * as Factory from './lib/factory.js' +import { BaseItemCounts } from './lib/BaseItemCounts.js' + chai.use(chaiAsPromised) const expect = chai.expect describe('item manager', function () { + let context + let application + + afterEach(async function () { + await context.deinit() + localStorage.clear() + }) + beforeEach(async function () { - this.payloadManager = new PayloadManager() - this.itemManager = new ItemManager(this.payloadManager) - this.createNote = async () => { - return this.itemManager.createItem(ContentType.Note, { - title: 'hello', - text: 'world', - }) - } + localStorage.clear() - this.createTag = async (notes = []) => { - const references = notes.map((note) => { - return { - uuid: note.uuid, - content_type: note.content_type, - } - }) - return this.itemManager.createItem(ContentType.Tag, { - title: 'thoughts', - references: references, - }) - } + context = await Factory.createAppContextWithFakeCrypto() + application = context.application + + await context.launch() }) - it('create item', async function () { - const item = await this.createNote() + const createNote = async () => { + return application.mutator.createItem(ContentType.Note, { + title: 'hello', + text: 'world', + }) + } - expect(item).to.be.ok - expect(item.title).to.equal('hello') - }) - - it('emitting item through payload and marking dirty should have userModifiedDate', async function () { - const payload = Factory.createNotePayload() - const item = await this.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) - const result = await this.itemManager.setItemDirty(item) - const appData = result.payload.content.appData - expect(appData[DecryptedItem.DefaultAppDomain()][AppDataField.UserModifiedDate]).to.be.ok - }) + const createTag = async (notes = []) => { + const references = notes.map((note) => { + return { + uuid: note.uuid, + content_type: note.content_type, + } + }) + return application.mutator.createItem(ContentType.Tag, { + title: 'thoughts', + references: references, + }) + } it('find items with valid uuid', async function () { - const item = await this.createNote() + const item = await createNote() - const results = await this.itemManager.findItems([item.uuid]) + const results = await application.items.findItems([item.uuid]) expect(results.length).to.equal(1) expect(results[0]).to.equal(item) }) it('find items with invalid uuid no blanks', async function () { - const results = await this.itemManager.findItems([Factory.generateUuidish()]) + const results = await application.items.findItems([Factory.generateUuidish()]) expect(results.length).to.equal(0) }) it('find items with invalid uuid include blanks', async function () { - const includeBlanks = true - const results = await this.itemManager.findItemsIncludingBlanks([Factory.generateUuidish()]) + const results = await application.items.findItemsIncludingBlanks([Factory.generateUuidish()]) expect(results.length).to.equal(1) expect(results[0]).to.not.be.ok }) it('item state', async function () { - await this.createNote() + await createNote() - expect(this.itemManager.items.length).to.equal(1) - expect(this.itemManager.getDisplayableNotes().length).to.equal(1) + expect(application.items.items.length).to.equal(1 + BaseItemCounts.DefaultItems) + expect(application.items.getDisplayableNotes().length).to.equal(1) }) it('find item', async function () { - const item = await this.createNote() + const item = await createNote() - const foundItem = this.itemManager.findItem(item.uuid) + const foundItem = application.items.findItem(item.uuid) expect(foundItem).to.be.ok }) it('reference map', async function () { - const note = await this.createNote() - const tag = await this.createTag([note]) + const note = await createNote() + const tag = await createTag([note]) - expect(this.itemManager.collection.referenceMap.directMap.get(tag.uuid)).to.eql([note.uuid]) + expect(application.items.collection.referenceMap.directMap.get(tag.uuid)).to.eql([note.uuid]) }) it('inverse reference map', async function () { - const note = await this.createNote() - const tag = await this.createTag([note]) + const note = await createNote() + const tag = await createTag([note]) - expect(this.itemManager.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([tag.uuid]) + expect(application.items.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([tag.uuid]) }) it('inverse reference map should not have duplicates', async function () { - const note = await this.createNote() - const tag = await this.createTag([note]) - await this.itemManager.changeItem(tag) + const note = await createNote() + const tag = await createTag([note]) + await application.mutator.changeItem(tag) - expect(this.itemManager.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([tag.uuid]) - }) - - it('deleting from reference map', async function () { - const note = await this.createNote() - const tag = await this.createTag([note]) - await this.itemManager.setItemToBeDeleted(note) - - expect(this.itemManager.collection.referenceMap.directMap.get(tag.uuid)).to.eql([]) - expect(this.itemManager.collection.referenceMap.inverseMap.get(note.uuid).length).to.equal(0) - }) - - it('deleting referenced item should update referencing item references', async function () { - const note = await this.createNote() - let tag = await this.createTag([note]) - await this.itemManager.setItemToBeDeleted(note) - - tag = this.itemManager.findItem(tag.uuid) - expect(tag.content.references.length).to.equal(0) - }) - - it('removing relationship should update reference map', async function () { - const note = await this.createNote() - const tag = await this.createTag([note]) - await this.itemManager.changeItem(tag, (mutator) => { - mutator.removeItemAsRelationship(note) - }) - - expect(this.itemManager.collection.referenceMap.directMap.get(tag.uuid)).to.eql([]) - expect(this.itemManager.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([]) - }) - - it('emitting discardable payload should remove it from our collection', async function () { - const note = await this.createNote() - - const payload = new DeletedPayload({ - ...note.payload.ejected(), - content: undefined, - deleted: true, - dirty: false, - }) - - expect(payload.discardable).to.equal(true) - - await this.itemManager.emitItemFromPayload(payload) - - expect(this.itemManager.findItem(note.uuid)).to.not.be.ok + expect(application.items.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([tag.uuid]) }) it('items that reference item', async function () { - const note = await this.createNote() - const tag = await this.createTag([note]) + const note = await createNote() + const tag = await createTag([note]) - const itemsThatReference = this.itemManager.itemsReferencingItem(note) + const itemsThatReference = application.items.itemsReferencingItem(note) expect(itemsThatReference.length).to.equal(1) expect(itemsThatReference[0]).to.equal(tag) }) it('observer', async function () { const observed = [] - this.itemManager.addObserver(ContentType.Any, ({ changed, inserted, removed, source, sourceKey }) => { + application.items.addObserver(ContentType.Any, ({ changed, inserted, removed, source, sourceKey }) => { observed.push({ changed, inserted, removed, source, sourceKey }) }) - const note = await this.createNote() - const tag = await this.createTag([note]) + const note = await createNote() + const tag = await createTag([note]) expect(observed.length).to.equal(2) const firstObserved = observed[0] @@ -171,59 +124,23 @@ describe('item manager', function () { expect(secondObserved.inserted).to.eql([tag]) }) - it('change existing item', async function () { - const note = await this.createNote() - const newTitle = String(Math.random()) - await this.itemManager.changeItem(note, (mutator) => { - mutator.title = newTitle - }) - - const latestVersion = this.itemManager.findItem(note.uuid) - expect(latestVersion.title).to.equal(newTitle) - }) - - it('change non-existant item through uuid should fail', async function () { - const note = await this.itemManager.createTemplateItem(ContentType.Note, { - title: 'hello', - text: 'world', - }) - - const changeFn = async () => { - const newTitle = String(Math.random()) - return this.itemManager.changeItem(note, (mutator) => { - mutator.title = newTitle - }) - } - await Factory.expectThrowsAsync(() => changeFn(), 'Attempting to change non-existant item') - }) - - it('set items dirty', async function () { - const note = await this.createNote() - await this.itemManager.setItemDirty(note) - - const dirtyItems = this.itemManager.getDirtyItems() - expect(dirtyItems.length).to.equal(1) - expect(dirtyItems[0].uuid).to.equal(note.uuid) - expect(dirtyItems[0].dirty).to.equal(true) - }) - it('dirty items should not include errored items', async function () { - const note = await this.itemManager.setItemDirty(await this.createNote()) + const note = await application.mutator.setItemDirty(await createNote()) const errorred = new EncryptedPayload({ ...note.payload, content: '004:...', errorDecrypting: true, }) - await this.itemManager.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged) + await application.mutator.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged) - const dirtyItems = this.itemManager.getDirtyItems() + const dirtyItems = application.items.getDirtyItems() expect(dirtyItems.length).to.equal(0) }) it('dirty items should include errored items if they are being deleted', async function () { - const note = await this.itemManager.setItemDirty(await this.createNote()) + const note = await application.mutator.setItemDirty(await createNote()) const errorred = new DeletedPayload({ ...note.payload, content: undefined, @@ -231,181 +148,63 @@ describe('item manager', function () { deleted: true, }) - await this.itemManager.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged) + await application.mutator.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged) - const dirtyItems = this.itemManager.getDirtyItems() + const dirtyItems = application.items.getDirtyItems() expect(dirtyItems.length).to.equal(1) }) - describe('duplicateItem', async function () { - const sandbox = sinon.createSandbox() - - beforeEach(async function () { - this.emitPayloads = sandbox.spy(this.itemManager.payloadManager, 'emitPayloads') - }) - - afterEach(async function () { - sandbox.restore() - }) - - it('should duplicate the item and set the duplicate_of property', async function () { - const note = await this.createNote() - await this.itemManager.duplicateItem(note) - sinon.assert.calledTwice(this.emitPayloads) - - const originalNote = this.itemManager.getDisplayableNotes()[0] - const duplicatedNote = this.itemManager.getDisplayableNotes()[1] - - expect(this.itemManager.items.length).to.equal(2) - expect(this.itemManager.getDisplayableNotes().length).to.equal(2) - expect(originalNote.uuid).to.not.equal(duplicatedNote.uuid) - expect(originalNote.uuid).to.equal(duplicatedNote.duplicateOf) - expect(originalNote.uuid).to.equal(duplicatedNote.payload.duplicate_of) - expect(duplicatedNote.conflictOf).to.be.undefined - expect(duplicatedNote.payload.content.conflict_of).to.be.undefined - }) - - it('should duplicate the item and set the duplicate_of and conflict_of properties', async function () { - const note = await this.createNote() - await this.itemManager.duplicateItem(note, true) - sinon.assert.calledTwice(this.emitPayloads) - - const originalNote = this.itemManager.getDisplayableNotes()[0] - const duplicatedNote = this.itemManager.getDisplayableNotes()[1] - - expect(this.itemManager.items.length).to.equal(2) - expect(this.itemManager.getDisplayableNotes().length).to.equal(2) - expect(originalNote.uuid).to.not.equal(duplicatedNote.uuid) - expect(originalNote.uuid).to.equal(duplicatedNote.duplicateOf) - expect(originalNote.uuid).to.equal(duplicatedNote.payload.duplicate_of) - expect(originalNote.uuid).to.equal(duplicatedNote.conflictOf) - expect(originalNote.uuid).to.equal(duplicatedNote.payload.content.conflict_of) - }) - - it('duplicate item with relationships', async function () { - const note = await this.createNote() - const tag = await this.createTag([note]) - const duplicate = await this.itemManager.duplicateItem(tag) - - expect(duplicate.content.references).to.have.length(1) - expect(this.itemManager.items).to.have.length(3) - expect(this.itemManager.getDisplayableTags()).to.have.length(2) - }) - - it('adds duplicated item as a relationship to items referencing it', async function () { - const note = await this.createNote() - let tag = await this.createTag([note]) - const duplicateNote = await this.itemManager.duplicateItem(note) - expect(tag.content.references).to.have.length(1) - - tag = this.itemManager.findItem(tag.uuid) - const references = tag.content.references.map((ref) => ref.uuid) - expect(references).to.have.length(2) - expect(references).to.include(note.uuid, duplicateNote.uuid) - }) - - it('duplicates item with additional content', async function () { - const note = await this.itemManager.createItem(ContentType.Note, { - title: 'hello', - text: 'world', - }) - const duplicateNote = await this.itemManager.duplicateItem(note, false, { - title: 'hello (copy)', - }) - - expect(duplicateNote.title).to.equal('hello (copy)') - expect(duplicateNote.text).to.equal('world') - }) - }) - - it('set item deleted', async function () { - const note = await this.createNote() - await this.itemManager.setItemToBeDeleted(note) - - /** Items should never be mutated directly */ - expect(note.deleted).to.not.be.ok - - const latestVersion = this.payloadManager.findOne(note.uuid) - expect(latestVersion.deleted).to.equal(true) - expect(latestVersion.dirty).to.equal(true) - expect(latestVersion.content).to.not.be.ok - - /** Deleted items do not show up in item manager's public interface */ - expect(this.itemManager.items.length).to.equal(0) - expect(this.itemManager.getDisplayableNotes().length).to.equal(0) - }) - it('system smart views', async function () { - expect(this.itemManager.systemSmartViews.length).to.be.above(0) + expect(application.items.systemSmartViews.length).to.be.above(0) }) it('find tag by title', async function () { - const tag = await this.createTag() + const tag = await createTag() - expect(this.itemManager.findTagByTitle(tag.title)).to.be.ok + expect(application.items.findTagByTitle(tag.title)).to.be.ok }) it('find tag by title should be case insensitive', async function () { - const tag = await this.createTag() + const tag = await createTag() - expect(this.itemManager.findTagByTitle(tag.title.toUpperCase())).to.be.ok + expect(application.items.findTagByTitle(tag.title.toUpperCase())).to.be.ok }) it('find or create tag by title', async function () { const title = 'foo' - expect(await this.itemManager.findOrCreateTagByTitle(title)).to.be.ok + expect(await application.mutator.findOrCreateTagByTitle({ title: title })).to.be.ok }) it('note count', async function () { - await this.createNote() - expect(this.itemManager.noteCount).to.equal(1) - }) - - it('trash', async function () { - const note = await this.createNote() - const versionTwo = await this.itemManager.changeItem(note, (mutator) => { - mutator.trashed = true - }) - - expect(this.itemManager.trashSmartView).to.be.ok - expect(versionTwo.trashed).to.equal(true) - expect(versionTwo.dirty).to.equal(true) - expect(versionTwo.content).to.be.ok - - expect(this.itemManager.items.length).to.equal(1) - expect(this.itemManager.trashedItems.length).to.equal(1) - - await this.itemManager.emptyTrash() - const versionThree = this.payloadManager.findOne(note.uuid) - expect(versionThree.deleted).to.equal(true) - expect(this.itemManager.trashedItems.length).to.equal(0) + await createNote() + expect(application.items.noteCount).to.equal(1) }) it('remove all items from memory', async function () { const observed = [] - this.itemManager.addObserver(ContentType.Any, ({ changed, inserted, removed, ignored }) => { + application.items.addObserver(ContentType.Any, ({ changed, inserted, removed, ignored }) => { observed.push({ changed, inserted, removed, ignored }) }) - await this.createNote() - await this.itemManager.removeAllItemsFromMemory() + await createNote() + await application.items.removeAllItemsFromMemory() const deletionEvent = observed[1] expect(deletionEvent.removed[0].deleted).to.equal(true) - expect(this.itemManager.items.length).to.equal(0) + expect(application.items.items.length).to.equal(0) }) it('remove item locally', async function () { const observed = [] - this.itemManager.addObserver(ContentType.Any, ({ changed, inserted, removed, ignored }) => { + application.items.addObserver(ContentType.Any, ({ changed, inserted, removed, ignored }) => { observed.push({ changed, inserted, removed, ignored }) }) - const note = await this.createNote() - await this.itemManager.removeItemLocally(note) + const note = await createNote() + await application.items.removeItemLocally(note) expect(observed.length).to.equal(1) - expect(this.itemManager.findItem(note.uuid)).to.not.be.ok + expect(application.items.findItem(note.uuid)).to.not.be.ok }) it('emitting a payload from within observer should queue to end', async function () { @@ -421,7 +220,7 @@ describe('item manager', function () { const changedTitle = 'changed title' let didEmit = false let latestVersion - this.itemManager.addObserver(ContentType.Note, ({ changed, inserted }) => { + application.items.addObserver(ContentType.Note, ({ changed, inserted }) => { const all = changed.concat(inserted) if (!didEmit) { didEmit = true @@ -431,60 +230,60 @@ describe('item manager', function () { title: changedTitle, }, }) - this.itemManager.emitItemFromPayload(changedPayload) + application.mutator.emitItemFromPayload(changedPayload) } latestVersion = all[0] }) - await this.itemManager.emitItemFromPayload(payload) + await application.mutator.emitItemFromPayload(payload) expect(latestVersion.title).to.equal(changedTitle) }) describe('searchTags', async function () { it('should return tag with query matching title', async function () { - const tag = await this.itemManager.findOrCreateTagByTitle('tag') + const tag = await application.mutator.findOrCreateTagByTitle({ title: 'tag' }) - const results = this.itemManager.searchTags('tag') + const results = application.items.searchTags('tag') expect(results).lengthOf(1) expect(results[0].title).to.equal(tag.title) }) it('should return all tags with query partially matching title', async function () { - const firstTag = await this.itemManager.findOrCreateTagByTitle('tag one') - const secondTag = await this.itemManager.findOrCreateTagByTitle('tag two') + const firstTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag one' }) + const secondTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag two' }) - const results = this.itemManager.searchTags('tag') + const results = application.items.searchTags('tag') expect(results).lengthOf(2) expect(results[0].title).to.equal(firstTag.title) expect(results[1].title).to.equal(secondTag.title) }) it('should be case insensitive', async function () { - const tag = await this.itemManager.findOrCreateTagByTitle('Tag') + const tag = await application.mutator.findOrCreateTagByTitle({ title: 'Tag' }) - const results = this.itemManager.searchTags('tag') + const results = application.items.searchTags('tag') expect(results).lengthOf(1) expect(results[0].title).to.equal(tag.title) }) it('should return tag with query matching delimiter separated component', async function () { - const tag = await this.itemManager.findOrCreateTagByTitle('parent.child') + const tag = await application.mutator.findOrCreateTagByTitle({ title: 'parent.child' }) - const results = this.itemManager.searchTags('child') + const results = application.items.searchTags('child') expect(results).lengthOf(1) expect(results[0].title).to.equal(tag.title) }) it('should return tags with matching query including delimiter', async function () { - const tag = await this.itemManager.findOrCreateTagByTitle('parent.child') + const tag = await application.mutator.findOrCreateTagByTitle({ title: 'parent.child' }) - const results = this.itemManager.searchTags('parent.chi') + const results = application.items.searchTags('parent.chi') expect(results).lengthOf(1) expect(results[0].title).to.equal(tag.title) }) it('should return tags in natural order', async function () { - const firstTag = await this.itemManager.findOrCreateTagByTitle('tag 100') - const secondTag = await this.itemManager.findOrCreateTagByTitle('tag 2') - const thirdTag = await this.itemManager.findOrCreateTagByTitle('tag b') - const fourthTag = await this.itemManager.findOrCreateTagByTitle('tag a') + const firstTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag 100' }) + const secondTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag 2' }) + const thirdTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag b' }) + const fourthTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag a' }) - const results = this.itemManager.searchTags('tag') + const results = application.items.searchTags('tag') expect(results).lengthOf(4) expect(results[0].title).to.equal(secondTag.title) expect(results[1].title).to.equal(firstTag.title) @@ -493,15 +292,15 @@ describe('item manager', function () { }) it('should not return tags associated with note', async function () { - const firstTag = await this.itemManager.findOrCreateTagByTitle('tag one') - const secondTag = await this.itemManager.findOrCreateTagByTitle('tag two') + const firstTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag one' }) + const secondTag = await application.mutator.findOrCreateTagByTitle({ title: 'tag two' }) - const note = await this.createNote() - await this.itemManager.changeItem(firstTag, (mutator) => { + const note = await createNote() + await application.mutator.changeItem(firstTag, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(note) }) - const results = this.itemManager.searchTags('tag', note) + const results = application.items.searchTags('tag', note) expect(results).lengthOf(1) expect(results[0].title).to.equal(secondTag.title) }) @@ -509,68 +308,68 @@ describe('item manager', function () { describe('smart views', async function () { it('all view should not include archived notes by default', async function () { - const normal = await this.createNote() + const normal = await createNote() - await this.itemManager.changeItem(normal, (mutator) => { + await application.mutator.changeItem(normal, (mutator) => { mutator.archived = true }) - this.itemManager.setPrimaryItemDisplayOptions({ - views: [this.itemManager.allNotesSmartView], + application.items.setPrimaryItemDisplayOptions({ + views: [application.items.allNotesSmartView], }) - expect(this.itemManager.getDisplayableNotes().length).to.equal(0) + expect(application.items.getDisplayableNotes().length).to.equal(0) }) it('archived view should not include trashed notes by default', async function () { - const normal = await this.createNote() + const normal = await createNote() - await this.itemManager.changeItem(normal, (mutator) => { + await application.mutator.changeItem(normal, (mutator) => { mutator.archived = true mutator.trashed = true }) - this.itemManager.setPrimaryItemDisplayOptions({ - views: [this.itemManager.archivedSmartView], + application.items.setPrimaryItemDisplayOptions({ + views: [application.items.archivedSmartView], }) - expect(this.itemManager.getDisplayableNotes().length).to.equal(0) + expect(application.items.getDisplayableNotes().length).to.equal(0) }) it('trashed view should include archived notes by default', async function () { - const normal = await this.createNote() + const normal = await createNote() - await this.itemManager.changeItem(normal, (mutator) => { + await application.mutator.changeItem(normal, (mutator) => { mutator.archived = true mutator.trashed = true }) - this.itemManager.setPrimaryItemDisplayOptions({ - views: [this.itemManager.trashSmartView], + application.items.setPrimaryItemDisplayOptions({ + views: [application.items.trashSmartView], }) - expect(this.itemManager.getDisplayableNotes().length).to.equal(1) + expect(application.items.getDisplayableNotes().length).to.equal(1) }) }) describe('getSortedTagsForNote', async function () { it('should return tags associated with a note in natural order', async function () { const tags = [ - await this.itemManager.findOrCreateTagByTitle('tag 100'), - await this.itemManager.findOrCreateTagByTitle('tag 2'), - await this.itemManager.findOrCreateTagByTitle('tag b'), - await this.itemManager.findOrCreateTagByTitle('tag a'), + await application.mutator.findOrCreateTagByTitle({ title: 'tag 100' }), + await application.mutator.findOrCreateTagByTitle({ title: 'tag 2' }), + await application.mutator.findOrCreateTagByTitle({ title: 'tag b' }), + await application.mutator.findOrCreateTagByTitle({ title: 'tag a' }), ] - const note = await this.createNote() + const note = await createNote() tags.map(async (tag) => { - await this.itemManager.changeItem(tag, (mutator) => { + await application.mutator.changeItem(tag, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(note) }) }) - const results = this.itemManager.getSortedTagsForItem(note) + const results = application.items.getSortedTagsForItem(note) expect(results).lengthOf(tags.length) expect(results[0].title).to.equal(tags[1].title) @@ -583,16 +382,16 @@ describe('item manager', function () { describe('getTagParentChain', function () { it('should return parent tags for a tag', async function () { const [parent, child, grandchild, _other] = await Promise.all([ - this.itemManager.findOrCreateTagByTitle('parent'), - this.itemManager.findOrCreateTagByTitle('parent.child'), - this.itemManager.findOrCreateTagByTitle('parent.child.grandchild'), - this.itemManager.findOrCreateTagByTitle('some other tag'), + application.mutator.findOrCreateTagByTitle({ title: 'parent' }), + application.mutator.findOrCreateTagByTitle({ title: 'parent.child' }), + application.mutator.findOrCreateTagByTitle({ title: 'parent.child.grandchild' }), + application.mutator.findOrCreateTagByTitle({ title: 'some other tag' }), ]) - await this.itemManager.setTagParent(parent, child) - await this.itemManager.setTagParent(child, grandchild) + await application.mutator.setTagParent(parent, child) + await application.mutator.setTagParent(child, grandchild) - const results = this.itemManager.getTagParentChain(grandchild) + const results = application.items.getTagParentChain(grandchild) expect(results).lengthOf(2) expect(results[0].uuid).to.equal(parent.uuid) diff --git a/packages/snjs/mocha/key_recovery_service.test.js b/packages/snjs/mocha/key_recovery_service.test.js index 74a426d25..d3db1febe 100644 --- a/packages/snjs/mocha/key_recovery_service.test.js +++ b/packages/snjs/mocha/key_recovery_service.test.js @@ -200,7 +200,9 @@ describe('key recovery service', function () { const receiveChallenge = (challenge) => { totalPromptCount++ /** Give unassociated password when prompted */ - application.submitValuesForChallenge(challenge, [CreateChallengeValue(challenge.prompts[0], unassociatedPassword)]) + application.submitValuesForChallenge(challenge, [ + CreateChallengeValue(challenge.prompts[0], unassociatedPassword), + ]) } await application.prepareForLaunch({ receiveChallenge }) await application.launch(true) @@ -272,7 +274,9 @@ describe('key recovery service', function () { expect(result.error).to.not.be.ok expect(contextB.application.items.getAnyItems(ContentType.ItemsKey).length).to.equal(2) - const newItemsKey = contextB.application.items.getDisplayableItemsKeys().find((k) => k.uuid !== originalItemsKey.uuid) + const newItemsKey = contextB.application.items + .getDisplayableItemsKeys() + .find((k) => k.uuid !== originalItemsKey.uuid) const note = await Factory.createSyncedNote(contextB.application) @@ -432,6 +436,7 @@ describe('key recovery service', function () { expect(decryptedKey.content.itemsKey).to.equal(correctItemsKey.content.itemsKey) expect(application.syncService.isOutOfSync()).to.equal(false) + await context.deinit() }) @@ -457,6 +462,8 @@ describe('key recovery service', function () { updated_at: newUpdated, }) + context.disableKeyRecovery() + await context.receiveServerResponse({ retrievedItems: [errored.ejected()] }) /** Our current items key should not be overwritten */ @@ -567,7 +574,9 @@ describe('key recovery service', function () { const application = context.application const receiveChallenge = (challenge) => { /** Give unassociated password when prompted */ - application.submitValuesForChallenge(challenge, [CreateChallengeValue(challenge.prompts[0], unassociatedPassword)]) + application.submitValuesForChallenge(challenge, [ + CreateChallengeValue(challenge.prompts[0], unassociatedPassword), + ]) } await application.prepareForLaunch({ receiveChallenge }) await application.launch(true) @@ -667,13 +676,15 @@ describe('key recovery service', function () { const stored = (await appA.deviceInterface.getAllDatabaseEntries(appA.identifier)).find( (payload) => payload.uuid === newDefaultKey.uuid, ) - const storedParams = await appA.protocolService.getKeyEmbeddedKeyParams(new EncryptedPayload(stored)) + const storedParams = await appA.protocolService.getKeyEmbeddedKeyParamsFromItemsKey(new EncryptedPayload(stored)) const correctStored = (await appB.deviceInterface.getAllDatabaseEntries(appB.identifier)).find( (payload) => payload.uuid === newDefaultKey.uuid, ) - const correctParams = await appB.protocolService.getKeyEmbeddedKeyParams(new EncryptedPayload(correctStored)) + const correctParams = await appB.protocolService.getKeyEmbeddedKeyParamsFromItemsKey( + new EncryptedPayload(correctStored), + ) expect(storedParams).to.eql(correctParams) diff --git a/packages/snjs/mocha/keys.test.js b/packages/snjs/mocha/keys.test.js index bbdd4d46b..f9ed45132 100644 --- a/packages/snjs/mocha/keys.test.js +++ b/packages/snjs/mocha/keys.test.js @@ -141,7 +141,8 @@ describe('keys', function () { }) it('should use items key for encryption of note', async function () { - const keyToUse = await this.application.protocolService.itemsEncryption.keyToUseForItemEncryption() + const notePayload = Factory.createNotePayload() + const keyToUse = await this.application.protocolService.itemsEncryption.keyToUseForItemEncryption(notePayload) expect(keyToUse.content_type).to.equal(ContentType.ItemsKey) }) @@ -153,7 +154,7 @@ describe('keys', function () { }, }) - const itemsKey = this.application.protocolService.itemsKeyForPayload(encryptedPayload) + const itemsKey = this.application.protocolService.itemsKeyForEncryptedPayload(encryptedPayload) expect(itemsKey).to.be.ok }) @@ -166,7 +167,7 @@ describe('keys', function () { }, }) - const itemsKey = this.application.protocolService.itemsKeyForPayload(encryptedPayload) + const itemsKey = this.application.protocolService.itemsKeyForEncryptedPayload(encryptedPayload) expect(itemsKey).to.be.ok const decryptedPayload = await this.application.protocolService.decryptSplitSingle({ @@ -187,7 +188,7 @@ describe('keys', function () { }, }) - const itemsKey = this.application.protocolService.itemsKeyForPayload(encryptedPayload) + const itemsKey = this.application.protocolService.itemsKeyForEncryptedPayload(encryptedPayload) await this.application.itemManager.removeItemLocally(itemsKey) @@ -197,14 +198,14 @@ describe('keys', function () { }, }) - await this.application.itemManager.emitItemsFromPayloads([erroredPayload], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([erroredPayload], PayloadEmitSource.LocalChanged) const note = this.application.itemManager.findAnyItem(notePayload.uuid) expect(note.errorDecrypting).to.equal(true) expect(note.waitingForKey).to.equal(true) const keyPayload = new DecryptedPayload(itemsKey.payload.ejected()) - await this.application.itemManager.emitItemsFromPayloads([keyPayload], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([keyPayload], PayloadEmitSource.LocalChanged) /** * Sleeping is required to trigger asyncronous protocolService.decryptItemsWaitingForKeys, @@ -238,7 +239,7 @@ describe('keys', function () { }, }) - await this.application.syncService.handleSuccessServerResponse({ payloadsSavedOrSaving: [] }, response) + await this.application.syncService.handleSuccessServerResponse({ payloadsSavedOrSaving: [], options: {} }, response) const refreshedKey = this.application.payloadManager.findOne(itemsKey.uuid) @@ -273,10 +274,8 @@ describe('keys', function () { const rawPayloads = await this.application.diskStorageService.getAllRawPayloads() const itemsKeyRawPayload = rawPayloads.find((p) => p.uuid === itemsKey.uuid) const itemsKeyPayload = new EncryptedPayload(itemsKeyRawPayload) - const operator = this.application.protocolService.operatorManager.operatorForVersion(ProtocolVersion.V004) - const comps = operator.deconstructEncryptedPayloadString(itemsKeyPayload.content) - const rawAuthenticatedData = comps.authenticatedData - const authenticatedData = await operator.stringToAuthenticatedData(rawAuthenticatedData) + + const authenticatedData = this.context.encryption.getEmbeddedPayloadAuthenticatedData(itemsKeyPayload) const rootKeyParams = await this.application.protocolService.getRootKeyParams() expect(authenticatedData.kp).to.be.ok @@ -649,7 +648,7 @@ describe('keys', function () { await contextB.deinit() }) - describe('changing password on 003 client while signed into 004 client should', function () { + describe('changing password on 003 client while signed into 004 client', function () { /** * When an 004 client signs into 003 account, it creates a root key based items key. * Then, if the 003 client changes its account password, and the 004 client @@ -658,7 +657,7 @@ describe('keys', function () { * items sync to the 004 client, it can't decrypt them with its existing items key * because its based on the old root key. */ - it.skip('add new items key', async function () { + it.skip('should add new items key', async function () { this.timeout(Factory.TwentySecondTimeout * 3) let oldClient = this.application @@ -718,7 +717,13 @@ describe('keys', function () { await Factory.safeDeinit(oldClient) }) - it('add new items key from migration if pw change already happened', async function () { + it('should add new items key from migration if pw change already happened', async function () { + this.context.anticipateConsoleError('Shared vault network errors due to not accepting JWT-based token') + this.context.anticipateConsoleError( + 'Cannot find items key to use for encryption', + 'No items keys being created in this test', + ) + /** Register an 003 account */ await Factory.registerOldUser({ application: this.application, @@ -734,7 +739,15 @@ describe('keys', function () { await this.application.protocolService.getRootKeyParams(), ) const operator = this.application.protocolService.operatorManager.operatorForVersion(ProtocolVersion.V003) - const newRootKey = await operator.createRootKey(this.email, this.password) + const newRootKeyTemplate = await operator.createRootKey(this.email, this.password) + const newRootKey = CreateNewRootKey({ + ...newRootKeyTemplate.content, + ...{ + encryptionKeyPair: {}, + signingKeyPair: {}, + }, + }) + Object.defineProperty(this.application.apiService, 'apiVersion', { get: function () { return '20190520' @@ -748,7 +761,7 @@ describe('keys', function () { currentServerPassword: currentRootKey.serverPassword, newRootKey, }) - await this.application.protocolService.reencryptItemsKeys() + await this.application.protocolService.reencryptApplicableItemsAfterUserRootKeyChange() /** Note: this may result in a deadlock if features_service syncs and results in an error */ await this.application.sync.sync({ awaitAll: true }) @@ -776,11 +789,16 @@ describe('keys', function () { * The corrective action was to do a final check in protocolService.handleDownloadFirstSyncCompletion * to ensure there exists an items key corresponding to the user's account version. */ + const promise = this.context.awaitNextSucessfulSync() + await this.context.sync() + await promise + await this.application.itemManager.removeAllItemsFromMemory() expect(this.application.protocolService.getSureDefaultItemsKey()).to.not.be.ok + const protocol003 = new SNProtocolOperator003(new SNWebCrypto()) const key = await protocol003.createItemsKey() - await this.application.itemManager.emitItemFromPayload( + await this.application.mutator.emitItemFromPayload( key.payload.copy({ content: { ...key.payload.content, @@ -791,17 +809,21 @@ describe('keys', function () { updated_at: Date.now(), }), ) + const defaultKey = this.application.protocolService.getSureDefaultItemsKey() expect(defaultKey.keyVersion).to.equal(ProtocolVersion.V003) expect(defaultKey.uuid).to.equal(key.uuid) + await Factory.registerUserToApplication({ application: this.application }) - expect(await this.application.protocolService.itemsEncryption.keyToUseForItemEncryption()).to.be.ok + + const notePayload = Factory.createNotePayload() + expect(await this.application.protocolService.itemsEncryption.keyToUseForItemEncryption(notePayload)).to.be.ok }) it('having unsynced items keys should resync them upon download first sync completion', async function () { await Factory.registerUserToApplication({ application: this.application }) const itemsKey = this.application.itemManager.getDisplayableItemsKeys()[0] - await this.application.itemManager.emitItemFromPayload( + await this.application.mutator.emitItemFromPayload( itemsKey.payload.copy({ dirty: false, updated_at: new Date(0), diff --git a/packages/snjs/mocha/lib/AppContext.js b/packages/snjs/mocha/lib/AppContext.js index d1107d3cd..e428f0b1e 100644 --- a/packages/snjs/mocha/lib/AppContext.js +++ b/packages/snjs/mocha/lib/AppContext.js @@ -2,6 +2,7 @@ import FakeWebCrypto from './fake_web_crypto.js' import * as Applications from './Applications.js' import * as Utils from './Utils.js' import * as Defaults from './Defaults.js' +import * as Events from './Events.js' import { createNotePayload } from './Items.js' UuidGenerator.SetGenerator(new FakeWebCrypto().generateUUID) @@ -11,6 +12,8 @@ const MaximumSyncOptions = { awaitAll: true, } +let GlobalSubscriptionIdCounter = 1001 + export class AppContext { constructor({ identifier, crypto, email, password, passcode, host } = {}) { this.identifier = identifier || `${Math.random()}` @@ -46,6 +49,62 @@ export class AppContext { ) } + get vaults() { + return this.application.vaultService + } + + get sessions() { + return this.application.sessions + } + + get items() { + return this.application.items + } + + get mutator() { + return this.application.mutator + } + + get payloads() { + return this.application.payloadManager + } + + get encryption() { + return this.application.protocolService + } + + get contacts() { + return this.application.contactService + } + + get sharedVaults() { + return this.application.sharedVaultService + } + + get files() { + return this.application.fileService + } + + get keys() { + return this.application.keySystemKeyManager + } + + get asymmetric() { + return this.application.asymmetricMessageService + } + + get publicKey() { + return this.sessions.getPublicKey() + } + + get signingPublicKey() { + return this.sessions.getSigningPublicKey() + } + + get privateKey() { + return this.encryption.getKeyPair().privateKey + } + ignoreChallenges() { this.ignoringChallenges = true } @@ -118,7 +177,10 @@ export class AppContext { }, }) - return this.application.syncService.handleSuccessServerResponse({ payloadsSavedOrSaving: [] }, response) + return this.application.syncService.handleSuccessServerResponse( + { payloadsSavedOrSaving: [], options: {} }, + response, + ) } resolveWhenKeyRecovered(uuid) { @@ -131,6 +193,16 @@ export class AppContext { }) } + resolveWhenSharedVaultUserKeysResolved() { + return new Promise((resolve) => { + this.application.vaultService.collaboration.addEventObserver((eventName) => { + if (eventName === SharedVaultServiceEvent.SharedVaultStatusChanged) { + resolve() + } + }) + }) + } + async awaitSignInEvent() { return new Promise((resolve) => { this.application.userService.addEventObserver((eventName) => { @@ -182,6 +254,155 @@ export class AppContext { }) } + awaitNextSyncSharedVaultFromScratchEvent() { + return new Promise((resolve) => { + const removeObserver = this.application.syncService.addEventObserver((event, data) => { + if (event === SyncEvent.PaginatedSyncRequestCompleted && data?.options?.sharedVaultUuids) { + removeObserver() + resolve(data) + } + }) + }) + } + + resolveWithUploadedPayloads() { + return new Promise((resolve) => { + this.application.syncService.addEventObserver((event, data) => { + if (event === SyncEvent.PaginatedSyncRequestCompleted) { + resolve(data.uploadedPayloads) + } + }) + }) + } + + resolveWithConflicts() { + return new Promise((resolve) => { + this.application.syncService.addEventObserver((event, response) => { + if (event === SyncEvent.PaginatedSyncRequestCompleted) { + resolve(response.rawConflictObjects) + } + }) + }) + } + + resolveWhenSavedSyncPayloadsIncludesItemUuid(uuid) { + return new Promise((resolve) => { + this.application.syncService.addEventObserver((event, response) => { + if (event === SyncEvent.PaginatedSyncRequestCompleted) { + const savedPayload = response.savedPayloads.find((payload) => payload.uuid === uuid) + if (savedPayload) { + resolve() + } + } + }) + }) + } + + resolveWhenSavedSyncPayloadsIncludesItemThatIsDuplicatedOf(uuid) { + return new Promise((resolve) => { + this.application.syncService.addEventObserver((event, response) => { + if (event === SyncEvent.PaginatedSyncRequestCompleted) { + const savedPayload = response.savedPayloads.find((payload) => payload.duplicate_of === uuid) + if (savedPayload) { + resolve() + } + } + }) + }) + } + + resolveWhenItemCompletesAddingToVault(targetItem) { + return new Promise((resolve) => { + const objectToSpy = this.vaults + sinon.stub(objectToSpy, 'moveItemToVault').callsFake(async (vault, item) => { + objectToSpy.moveItemToVault.restore() + const result = await objectToSpy.moveItemToVault(vault, item) + if (!targetItem || item.uuid === targetItem.uuid) { + resolve() + } + return result + }) + }) + } + + resolveWhenItemCompletesRemovingFromVault(targetItem) { + return new Promise((resolve) => { + const objectToSpy = this.vaults + sinon.stub(objectToSpy, 'removeItemFromVault').callsFake(async (item) => { + objectToSpy.removeItemFromVault.restore() + const result = await objectToSpy.removeItemFromVault(item) + if (item.uuid === targetItem.uuid) { + resolve() + } + return result + }) + }) + } + + resolveWhenAsymmetricMessageProcessingCompletes() { + return new Promise((resolve) => { + const objectToSpy = this.asymmetric + sinon.stub(objectToSpy, 'handleRemoteReceivedAsymmetricMessages').callsFake(async (messages) => { + objectToSpy.handleRemoteReceivedAsymmetricMessages.restore() + const result = await objectToSpy.handleRemoteReceivedAsymmetricMessages(messages) + resolve() + return result + }) + }) + } + + resolveWhenUserMessagesProcessingCompletes() { + return new Promise((resolve) => { + const objectToSpy = this.application.userEventService + sinon.stub(objectToSpy, 'handleReceivedUserEvents').callsFake(async (params) => { + objectToSpy.handleReceivedUserEvents.restore() + const result = await objectToSpy.handleReceivedUserEvents(params) + resolve() + return result + }) + }) + } + + resolveWhenSharedVaultServiceSendsContactShareMessage() { + return new Promise((resolve) => { + const objectToSpy = this.sharedVaults + sinon.stub(objectToSpy, 'shareContactWithUserAdministeredSharedVaults').callsFake(async (contact) => { + objectToSpy.shareContactWithUserAdministeredSharedVaults.restore() + const result = await objectToSpy.shareContactWithUserAdministeredSharedVaults(contact) + resolve() + return result + }) + }) + } + + resolveWhenSharedVaultKeyRotationInvitesGetSent(targetVault) { + return new Promise((resolve) => { + const objectToSpy = this.sharedVaults + sinon.stub(objectToSpy, 'handleVaultRootKeyRotatedEvent').callsFake(async (vault) => { + objectToSpy.handleVaultRootKeyRotatedEvent.restore() + const result = await objectToSpy.handleVaultRootKeyRotatedEvent(vault) + if (vault.systemIdentifier === targetVault.systemIdentifier) { + resolve() + } + return result + }) + }) + } + + resolveWhenSharedVaultChangeInvitesAreSent(sharedVaultUuid) { + return new Promise((resolve) => { + const objectToSpy = this.sharedVaults + sinon.stub(objectToSpy, 'handleVaultRootKeyRotatedEvent').callsFake(async (vault) => { + objectToSpy.handleVaultRootKeyRotatedEvent.restore() + const result = await objectToSpy.handleVaultRootKeyRotatedEvent(vault) + if (vault.sharing.sharedVaultUuid === sharedVaultUuid) { + resolve() + } + return result + }) + }) + } + awaitUserPrefsSingletonCreation() { const preferences = this.application.preferencesService.preferences if (preferences) { @@ -232,6 +453,10 @@ export class AppContext { await this.application.sync.sync(options || { awaitAll: true }) } + async clearSyncPositionTokens() { + await this.application.sync.clearSyncPositionTokens() + } + async maximumSync() { await this.sync(MaximumSyncOptions) } @@ -290,22 +515,31 @@ export class AppContext { }) } - async createSyncedNote(title, text) { + async createSyncedNote(title = 'foo', text = 'bar') { const payload = createNotePayload(title, text) - const item = await this.application.items.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) - await this.application.items.setItemDirty(item) + const item = await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) + await this.application.mutator.setItemDirty(item) await this.application.syncService.sync(MaximumSyncOptions) const note = this.application.items.findItem(payload.uuid) return note } + lockSyncing() { + this.application.syncService.lockSyncing() + } + + unlockSyncing() { + this.application.syncService.unlockSyncing() + } + async deleteItemAndSync(item) { await this.application.mutator.deleteItem(item) + await this.sync() } async changeNoteTitle(note, title) { - return this.application.items.changeNote(note, (mutator) => { + return this.application.mutator.changeNote(note, (mutator) => { mutator.title = title }) } @@ -325,6 +559,10 @@ export class AppContext { return this.application.items.getDisplayableNotes().length } + get notes() { + return this.application.items.getDisplayableNotes() + } + async createConflictedNotes(otherContext) { const note = await this.createSyncedNote() @@ -341,4 +579,41 @@ export class AppContext { conflict: this.findNoteByTitle('title-2'), } } + + findDuplicateNote(duplicateOfUuid) { + const items = this.items.getDisplayableNotes() + return items.find((note) => note.duplicateOf === duplicateOfUuid) + } + + get userUuid() { + return this.application.sessions.user.uuid + } + + sleep(seconds) { + return Utils.sleep(seconds) + } + + anticipateConsoleError(message, _reason) { + console.warn('Anticipating a console error with message:', message) + } + + async publicMockSubscriptionPurchaseEvent() { + await Events.publishMockedEvent('SUBSCRIPTION_PURCHASED', { + userEmail: this.email, + subscriptionId: GlobalSubscriptionIdCounter++, + subscriptionName: 'PRO_PLAN', + subscriptionExpiresAt: (new Date().getTime() + 3_600_000) * 1_000, + timestamp: Date.now(), + offline: false, + discountCode: null, + limitedDiscountPurchased: false, + newSubscriber: true, + totalActiveSubscriptionsCount: 1, + userRegisteredAt: 1, + billingFrequency: 12, + payAmount: 59.0, + }) + + await Utils.sleep(2) + } } diff --git a/packages/snjs/mocha/lib/Applications.js b/packages/snjs/mocha/lib/Applications.js index a606bc692..d0d4fac53 100644 --- a/packages/snjs/mocha/lib/Applications.js +++ b/packages/snjs/mocha/lib/Applications.js @@ -2,10 +2,6 @@ import WebDeviceInterface from './web_device_interface.js' import FakeWebCrypto from './fake_web_crypto.js' import * as Defaults from './Defaults.js' -export const BaseItemCounts = { - DefaultItems: ['ItemsKey', 'UserPreferences', 'DarkTheme'].length, -} - export function createApplicationWithOptions({ identifier, environment, platform, host, crypto, device }) { if (!device) { device = new WebDeviceInterface() diff --git a/packages/snjs/mocha/lib/BaseItemCounts.js b/packages/snjs/mocha/lib/BaseItemCounts.js new file mode 100644 index 000000000..837ca8d6c --- /dev/null +++ b/packages/snjs/mocha/lib/BaseItemCounts.js @@ -0,0 +1,35 @@ +const ExpectedItemCountsWithVaultFeatureEnabled = { + Items: ['ItemsKey', 'UserPreferences', 'DarkTheme'].length, + ItemsWithAccount: ['ItemsKey', 'UserPreferences', 'DarkTheme', 'TrustedSelfContact'].length, + ItemsWithAccountWithoutItemsKey: ['UserPreferences', 'DarkTheme', 'TrustedSelfContact'].length, + ItemsNoAccounNoItemsKey: ['UserPreferences', 'DarkTheme'].length, + BackupFileRootKeyEncryptedItems: ['TrustedSelfContact'].length, +} + +const ExpectedItemCountsWithVaultFeatureDisabled = { + Items: ['ItemsKey', 'UserPreferences', 'DarkTheme'].length, + ItemsWithAccount: ['ItemsKey', 'UserPreferences', 'DarkTheme'].length, + ItemsWithAccountWithoutItemsKey: ['UserPreferences', 'DarkTheme'].length, + ItemsNoAccounNoItemsKey: ['UserPreferences', 'DarkTheme'].length, + BackupFileRootKeyEncryptedItems: [].length, +} + +const isVaultsEnabled = InternalFeatureService.get().isFeatureEnabled(InternalFeature.Vaults) + +export const BaseItemCounts = { + DefaultItems: isVaultsEnabled + ? ExpectedItemCountsWithVaultFeatureEnabled.Items + : ExpectedItemCountsWithVaultFeatureDisabled.Items, + DefaultItemsWithAccount: isVaultsEnabled + ? ExpectedItemCountsWithVaultFeatureEnabled.ItemsWithAccount + : ExpectedItemCountsWithVaultFeatureDisabled.ItemsWithAccount, + DefaultItemsWithAccountWithoutItemsKey: isVaultsEnabled + ? ExpectedItemCountsWithVaultFeatureEnabled.ItemsWithAccountWithoutItemsKey + : ExpectedItemCountsWithVaultFeatureDisabled.ItemsWithAccountWithoutItemsKey, + DefaultItemsNoAccounNoItemsKey: isVaultsEnabled + ? ExpectedItemCountsWithVaultFeatureEnabled.ItemsNoAccounNoItemsKey + : ExpectedItemCountsWithVaultFeatureDisabled.ItemsNoAccounNoItemsKey, + BackupFileRootKeyEncryptedItems: isVaultsEnabled + ? ExpectedItemCountsWithVaultFeatureEnabled.BackupFileRootKeyEncryptedItems + : ExpectedItemCountsWithVaultFeatureDisabled.BackupFileRootKeyEncryptedItems, +} diff --git a/packages/snjs/mocha/lib/Collaboration.js b/packages/snjs/mocha/lib/Collaboration.js new file mode 100644 index 000000000..c3328e0d4 --- /dev/null +++ b/packages/snjs/mocha/lib/Collaboration.js @@ -0,0 +1,140 @@ +import * as Factory from './factory.js' + +export const createContactContext = async () => { + const contactContext = await Factory.createAppContextWithRealCrypto() + await contactContext.launch() + await contactContext.register() + + return { + contactContext, + deinitContactContext: contactContext.deinit.bind(contactContext), + } +} + +export const createTrustedContactForUserOfContext = async ( + contextAddingNewContact, + contextImportingContactInfoFrom, +) => { + const contact = await contextAddingNewContact.application.contactService.createOrEditTrustedContact({ + name: 'John Doe', + publicKey: contextImportingContactInfoFrom.publicKey, + signingPublicKey: contextImportingContactInfoFrom.signingPublicKey, + contactUuid: contextImportingContactInfoFrom.userUuid, + }) + + return contact +} + +export const acceptAllInvites = async (context) => { + const inviteRecords = context.sharedVaults.getCachedPendingInviteRecords() + for (const record of inviteRecords) { + await context.sharedVaults.acceptPendingSharedVaultInvite(record) + } +} + +export const createSharedVaultWithAcceptedInvite = async (context, permissions = SharedVaultPermission.Write) => { + const { sharedVault, contact, contactContext, deinitContactContext } = + await createSharedVaultWithUnacceptedButTrustedInvite(context, permissions) + + const promise = contactContext.awaitNextSyncSharedVaultFromScratchEvent() + + await acceptAllInvites(contactContext) + + await promise + + const contactVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) + + return { sharedVault, contact, contactVault, contactContext, deinitContactContext } +} + +export const createSharedVaultWithAcceptedInviteAndNote = async ( + context, + permissions = SharedVaultPermission.Write, +) => { + const { sharedVault, contactContext, contact, deinitContactContext } = await createSharedVaultWithAcceptedInvite( + context, + permissions, + ) + const note = await context.createSyncedNote('foo', 'bar') + const updatedNote = await moveItemToVault(context, sharedVault, note) + await contactContext.sync() + + return { sharedVault, note: updatedNote, contact, contactContext, deinitContactContext } +} + +export const createSharedVaultWithUnacceptedButTrustedInvite = async ( + context, + permissions = SharedVaultPermission.Write, +) => { + const sharedVault = await createSharedVault(context) + + const { contactContext, deinitContactContext } = await createContactContext() + const contact = await createTrustedContactForUserOfContext(context, contactContext) + await createTrustedContactForUserOfContext(contactContext, context) + + const invite = await context.sharedVaults.inviteContactToSharedVault(sharedVault, contact, permissions) + await contactContext.sync() + + return { sharedVault, contact, contactContext, deinitContactContext, invite } +} + +export const createSharedVaultWithUnacceptedAndUntrustedInvite = async ( + context, + permissions = SharedVaultPermission.Write, +) => { + const sharedVault = await createSharedVault(context) + + const { contactContext, deinitContactContext } = await createContactContext() + const contact = await createTrustedContactForUserOfContext(context, contactContext) + + const invite = await context.sharedVaults.inviteContactToSharedVault(sharedVault, contact, permissions) + await contactContext.sync() + + return { sharedVault, contact, contactContext, deinitContactContext, invite } +} + +export const inviteNewPartyToSharedVault = async (context, sharedVault, permissions = SharedVaultPermission.Write) => { + const { contactContext: thirdPartyContext, deinitContactContext: deinitThirdPartyContext } = + await createContactContext() + + const thirdPartyContact = await createTrustedContactForUserOfContext(context, thirdPartyContext) + await createTrustedContactForUserOfContext(thirdPartyContext, context) + await context.sharedVaults.inviteContactToSharedVault(sharedVault, thirdPartyContact, permissions) + + await thirdPartyContext.sync() + + return { thirdPartyContext, thirdPartyContact, deinitThirdPartyContext } +} + +export const createPrivateVault = async (context) => { + const privateVault = await context.vaults.createRandomizedVault({ + name: 'My Private Vault', + storagePreference: KeySystemRootKeyStorageMode.Synced, + }) + + return privateVault +} + +export const createSharedVault = async (context) => { + const sharedVault = await context.sharedVaults.createSharedVault({ name: 'My Shared Vault' }) + + if (isClientDisplayableError(sharedVault)) { + throw new Error(sharedVault.text) + } + + return sharedVault +} + +export const createSharedVaultWithNote = async (context) => { + const sharedVault = await createSharedVault(context) + const note = await context.createSyncedNote() + const updatedNote = await moveItemToVault(context, sharedVault, note) + return { sharedVault, note: updatedNote } +} + +export const moveItemToVault = async (context, sharedVault, item) => { + const promise = context.resolveWhenItemCompletesAddingToVault(item) + const updatedItem = await context.vaults.moveItemToVault(sharedVault, item) + await promise + return updatedItem +} diff --git a/packages/snjs/mocha/lib/Events.js b/packages/snjs/mocha/lib/Events.js new file mode 100644 index 000000000..7deba7546 --- /dev/null +++ b/packages/snjs/mocha/lib/Events.js @@ -0,0 +1,19 @@ +import * as Defaults from './Defaults.js' + +export async function publishMockedEvent(eventType, eventPayload) { + const response = await fetch(`${Defaults.getDefaultMockedEventServiceUrl()}/events`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + eventType, + eventPayload, + }), + }) + + if (!response.ok) { + console.error(`Failed to publish mocked event: ${response.status} ${response.statusText}`) + } +} diff --git a/packages/snjs/mocha/lib/Files.js b/packages/snjs/mocha/lib/Files.js index 6c4cabfcd..954965bb0 100644 --- a/packages/snjs/mocha/lib/Files.js +++ b/packages/snjs/mocha/lib/Files.js @@ -1,5 +1,5 @@ -export async function uploadFile(fileService, buffer, name, ext, chunkSize) { - const operation = await fileService.beginNewFileUpload(buffer.byteLength) +export async function uploadFile(fileService, buffer, name, ext, chunkSize, vault) { + const operation = await fileService.beginNewFileUpload(buffer.byteLength, vault) let chunkId = 1 for (let i = 0; i < buffer.length; i += chunkSize) { @@ -18,14 +18,16 @@ export async function uploadFile(fileService, buffer, name, ext, chunkSize) { return file } -export async function downloadFile(fileService, itemManager, remoteIdentifier) { - const file = itemManager.getItems(ContentType.File).find((file) => file.remoteIdentifier === remoteIdentifier) - +export async function downloadFile(fileService, file) { let receivedBytes = new Uint8Array() - await fileService.downloadFile(file, (decryptedBytes) => { + const error = await fileService.downloadFile(file, (decryptedBytes) => { receivedBytes = new Uint8Array([...receivedBytes, ...decryptedBytes]) }) + if (error) { + throw new Error('Could not download file', error.text) + } + return receivedBytes } diff --git a/packages/snjs/mocha/lib/Items.js b/packages/snjs/mocha/lib/Items.js index 09c4e9841..d10a693bc 100644 --- a/packages/snjs/mocha/lib/Items.js +++ b/packages/snjs/mocha/lib/Items.js @@ -63,7 +63,7 @@ export function createRelatedNoteTagPairPayload({ noteTitle, noteText, tagTitle, export async function createSyncedNoteWithTag(application) { const payloads = createRelatedNoteTagPairPayload() - await application.itemManager.emitItemsFromPayloads(payloads) + await application.mutator.emitItemsFromPayloads(payloads) return application.sync.sync(MaximumSyncOptions) } diff --git a/packages/snjs/mocha/lib/factory.js b/packages/snjs/mocha/lib/factory.js index 7f60369f3..45e8b8f71 100644 --- a/packages/snjs/mocha/lib/factory.js +++ b/packages/snjs/mocha/lib/factory.js @@ -71,24 +71,6 @@ export function getDefaultHost() { return Defaults.getDefaultHost() } -export async function publishMockedEvent(eventType, eventPayload) { - const response = await fetch(`${Defaults.getDefaultMockedEventServiceUrl()}/events`, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - eventType, - eventPayload, - }), - }) - - if (!response.ok) { - console.error(`Failed to publish mocked event: ${response.status} ${response.statusText}`) - } -} - export function createApplicationWithFakeCrypto(identifier, environment, platform, host) { return Applications.createApplicationWithFakeCrypto(identifier, environment, platform, host) } @@ -154,7 +136,7 @@ export async function registerOldUser({ application, email, password, version }) keyParams: accountKey.keyParams, }) /** Mark all existing items as dirty. */ - await application.itemManager.changeItems(application.itemManager.items, (m) => { + await application.mutator.changeItems(application.itemManager.items, (m) => { m.dirty = true }) await application.sessionManager.handleSuccessAuthResponse(response, accountKey) @@ -188,18 +170,18 @@ export function itemToStoragePayload(item) { export function createMappedNote(application, title, text, dirty = true) { const payload = createNotePayload(title, text, dirty) - return application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) + return application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) } export async function createMappedTag(application, tagParams = {}) { const payload = createStorageItemTagPayload(tagParams) - return application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) + return application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) } export async function createSyncedNote(application, title, text) { const payload = createNotePayload(title, text) - const item = await application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) - await application.itemManager.setItemDirty(item) + const item = await application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) + await application.mutator.setItemDirty(item) await application.syncService.sync(syncOptions) const note = application.items.findItem(payload.uuid) return note @@ -218,7 +200,7 @@ export async function createManyMappedNotes(application, count) { const createdNotes = [] for (let i = 0; i < count; i++) { const note = await createMappedNote(application) - await application.itemManager.setItemDirty(note) + await application.mutator.setItemDirty(note) createdNotes.push(note) } return createdNotes @@ -406,7 +388,7 @@ export function pinNote(application, note) { } export async function insertItemWithOverride(application, contentType, content, needsSync = false, errorDecrypting) { - const item = await application.itemManager.createItem(contentType, content, needsSync) + const item = await application.mutator.createItem(contentType, content, needsSync) if (errorDecrypting) { const encrypted = new EncryptedPayload({ @@ -415,12 +397,12 @@ export async function insertItemWithOverride(application, contentType, content, errorDecrypting, }) - await application.itemManager.emitItemFromPayload(encrypted) + await application.payloadManager.emitPayload(encrypted) } else { const decrypted = new DecryptedPayload({ ...item.payload.ejected(), }) - await application.itemManager.emitItemFromPayload(decrypted) + await application.payloadManager.emitPayload(decrypted) } return application.itemManager.findAnyItem(item.uuid) @@ -441,7 +423,7 @@ export async function markDirtyAndSyncItem(application, itemToLookupUuidFor) { throw Error('Attempting to save non-inserted item') } if (!item.dirty) { - await application.itemManager.changeItem(item, undefined, MutationType.NoUpdateUserTimestamps) + await application.mutator.changeItem(item, undefined, MutationType.NoUpdateUserTimestamps) } await application.sync.sync() } @@ -467,23 +449,22 @@ export async function changePayloadTimeStamp(application, payload, timestamp, co updated_at_timestamp: timestamp, }) - await application.itemManager.emitItemFromPayload(changedPayload) + await application.mutator.emitItemFromPayload(changedPayload) return application.itemManager.findAnyItem(payload.uuid) } export async function changePayloadUpdatedAt(application, payload, updatedAt) { const latestPayload = application.payloadManager.collection.find(payload.uuid) + const changedPayload = new DecryptedPayload({ - ...latestPayload, + ...latestPayload.ejected(), dirty: true, dirtyIndex: getIncrementedDirtyIndex(), updated_at: updatedAt, }) - await application.itemManager.emitItemFromPayload(changedPayload) - - return application.itemManager.findAnyItem(payload.uuid) + return application.mutator.emitItemFromPayload(changedPayload) } export async function changePayloadTimeStampDeleteAndSync(application, payload, timestamp, syncOptions) { @@ -497,6 +478,6 @@ export async function changePayloadTimeStampDeleteAndSync(application, payload, updated_at_timestamp: timestamp, }) - await application.itemManager.emitItemFromPayload(changedPayload) + await application.payloadManager.emitPayload(changedPayload) await application.sync.sync(syncOptions) } diff --git a/packages/snjs/mocha/lib/fake_web_crypto.js b/packages/snjs/mocha/lib/fake_web_crypto.js index f290a9e30..c898b693b 100644 --- a/packages/snjs/mocha/lib/fake_web_crypto.js +++ b/packages/snjs/mocha/lib/fake_web_crypto.js @@ -158,8 +158,39 @@ export default class FakeWebCrypto { return data.message } - sodiumCryptoBoxGenerateKeypair() { - return { publicKey: this.randomString(64), privateKey: this.randomString(64), keyType: 'x25519' } + sodiumCryptoSign(message, secretKey) { + const data = { + message, + secretKey, + } + return btoa(JSON.stringify(data)) + } + + sodiumCryptoKdfDeriveFromKey(key, subkeyNumber, subkeyLength, context) { + return btoa(key + subkeyNumber + subkeyLength + context) + } + + sodiumCryptoGenericHash(message, key) { + return btoa(message + key) + } + + sodiumCryptoSignVerify(message, signature, publicKey) { + return true + } + + sodiumCryptoBoxSeedKeypair(seed) { + return { + privateKey: seed, + publicKey: seed, + } + } + + + sodiumCryptoSignSeedKeypair(seed) { + return { + privateKey: seed, + publicKey: seed, + } } generateOtpSecret() { diff --git a/packages/snjs/mocha/lib/web_device_interface.js b/packages/snjs/mocha/lib/web_device_interface.js index 78daa9a25..9be392c8d 100644 --- a/packages/snjs/mocha/lib/web_device_interface.js +++ b/packages/snjs/mocha/lib/web_device_interface.js @@ -37,6 +37,10 @@ export default class WebDeviceInterface { return {} } + clearAllDataFromDevice() { + localStorage.clear() + } + _getDatabaseKeyPrefix(identifier) { if (identifier) { return `${identifier}-item-` @@ -61,29 +65,45 @@ export default class WebDeviceInterface { async getDatabaseLoadChunks(options, identifier) { const entries = await this.getAllDatabaseEntries(identifier) - const sorted = GetSortedPayloadsByPriority(entries, options) + const { + itemsKeyPayloads, + keySystemRootKeyPayloads, + keySystemItemsKeyPayloads, + contentTypePriorityPayloads, + remainingPayloads, + } = GetSortedPayloadsByPriority(entries, options) const itemsKeysChunk = { - entries: sorted.itemsKeyPayloads, + entries: itemsKeyPayloads, + } + + const keySystemRootKeysChunk = { + entries: keySystemRootKeyPayloads, + } + + const keySystemItemsKeysChunk = { + entries: keySystemItemsKeyPayloads, } const contentTypePriorityChunk = { - entries: sorted.contentTypePriorityPayloads, + entries: contentTypePriorityPayloads, } const remainingPayloadsChunks = [] - for (let i = 0; i < sorted.remainingPayloads.length; i += options.batchSize) { + for (let i = 0; i < remainingPayloads.length; i += options.batchSize) { remainingPayloadsChunks.push({ - entries: sorted.remainingPayloads.slice(i, i + options.batchSize), + entries: remainingPayloads.slice(i, i + options.batchSize), }) } const result = { fullEntries: { itemsKeys: itemsKeysChunk, + keySystemRootKeys: keySystemRootKeysChunk, + keySystemItemsKeys: keySystemItemsKeysChunk, remainingChunks: [contentTypePriorityChunk, ...remainingPayloadsChunks], }, - remainingChunksItemCount: sorted.contentTypePriorityPayloads.length + sorted.remainingPayloads.length, + remainingChunksItemCount: contentTypePriorityPayloads.length + remainingPayloads.length, } return result diff --git a/packages/snjs/mocha/migrations/migration.test.js b/packages/snjs/mocha/migrations/migration.test.js index d1ce10813..6c4fcdb5d 100644 --- a/packages/snjs/mocha/migrations/migration.test.js +++ b/packages/snjs/mocha/migrations/migration.test.js @@ -68,7 +68,7 @@ describe('migrations', () => { }), }), ) - await application.mutator.insertItem(mfaItem) + await application.mutator.insertItem(mfaItem, true) await application.sync.sync() expect(application.items.getItems('SF|MFA').length).to.equal(1) diff --git a/packages/snjs/mocha/model_tests/appmodels.test.js b/packages/snjs/mocha/model_tests/appmodels.test.js index 13847c349..f04ddc2f4 100644 --- a/packages/snjs/mocha/model_tests/appmodels.test.js +++ b/packages/snjs/mocha/model_tests/appmodels.test.js @@ -1,7 +1,7 @@ /* eslint-disable camelcase */ /* eslint-disable no-unused-expressions */ /* eslint-disable no-undef */ -import { BaseItemCounts } from '../lib/Applications.js' +import { BaseItemCounts } from '../lib/BaseItemCounts.js' import * as Factory from '../lib/factory.js' chai.use(chaiAsPromised) const expect = chai.expect @@ -70,8 +70,8 @@ describe('app models', () => { }, }) - await this.application.itemManager.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged) - await this.application.itemManager.emitItemsFromPayloads([params2], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([params2], PayloadEmitSource.LocalChanged) const item1 = this.application.itemManager.findItem(params1.uuid) const item2 = this.application.itemManager.findItem(params2.uuid) @@ -93,11 +93,11 @@ describe('app models', () => { }, }) - let items = await this.application.itemManager.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged) + let items = await this.application.mutator.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged) let item = items[0] expect(item).to.be.ok - items = await this.application.itemManager.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged) + items = await this.application.mutator.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged) item = items[0] expect(item.content.foo).to.equal('bar') @@ -108,10 +108,10 @@ describe('app models', () => { const item1 = await Factory.createMappedNote(this.application) const item2 = await Factory.createMappedNote(this.application) - await this.application.itemManager.changeItem(item1, (mutator) => { + await this.application.mutator.changeItem(item1, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(item2) }) - await this.application.itemManager.changeItem(item2, (mutator) => { + await this.application.mutator.changeItem(item2, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(item1) }) @@ -123,10 +123,10 @@ describe('app models', () => { var item1 = await Factory.createMappedNote(this.application) var item2 = await Factory.createMappedNote(this.application) - await this.application.itemManager.changeItem(item1, (mutator) => { + await this.application.mutator.changeItem(item1, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(item2) }) - await this.application.itemManager.changeItem(item2, (mutator) => { + await this.application.mutator.changeItem(item2, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(item1) }) @@ -143,7 +143,7 @@ describe('app models', () => { references: [], }, }) - await this.application.itemManager.emitItemsFromPayloads([damagedPayload], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([damagedPayload], PayloadEmitSource.LocalChanged) const refreshedItem1_2 = this.application.itemManager.findItem(item1.uuid) const refreshedItem2_2 = this.application.itemManager.findItem(item2.uuid) @@ -155,10 +155,10 @@ describe('app models', () => { it('creating and removing relationships between two items should have valid references', async function () { var item1 = await Factory.createMappedNote(this.application) var item2 = await Factory.createMappedNote(this.application) - await this.application.itemManager.changeItem(item1, (mutator) => { + await this.application.mutator.changeItem(item1, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(item2) }) - await this.application.itemManager.changeItem(item2, (mutator) => { + await this.application.mutator.changeItem(item2, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(item1) }) @@ -171,10 +171,10 @@ describe('app models', () => { expect(this.application.itemManager.itemsReferencingItem(item1)).to.include(refreshedItem2) expect(this.application.itemManager.itemsReferencingItem(item2)).to.include(refreshedItem1) - await this.application.itemManager.changeItem(item1, (mutator) => { + await this.application.mutator.changeItem(item1, (mutator) => { mutator.removeItemAsRelationship(item2) }) - await this.application.itemManager.changeItem(item2, (mutator) => { + await this.application.mutator.changeItem(item2, (mutator) => { mutator.removeItemAsRelationship(item1) }) @@ -190,7 +190,7 @@ describe('app models', () => { it('properly duplicates item with no relationships', async function () { const item = await Factory.createMappedNote(this.application) - const duplicate = await this.application.itemManager.duplicateItem(item) + const duplicate = await this.application.mutator.duplicateItem(item) expect(duplicate.uuid).to.not.equal(item.uuid) expect(item.isItemContentEqualWith(duplicate)).to.equal(true) expect(item.created_at.toISOString()).to.equal(duplicate.created_at.toISOString()) @@ -201,13 +201,13 @@ describe('app models', () => { const item1 = await Factory.createMappedNote(this.application) const item2 = await Factory.createMappedNote(this.application) - const refreshedItem1 = await this.application.itemManager.changeItem(item1, (mutator) => { + const refreshedItem1 = await this.application.mutator.changeItem(item1, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(item2) }) expect(refreshedItem1.content.references.length).to.equal(1) - const duplicate = await this.application.itemManager.duplicateItem(item1) + const duplicate = await this.application.mutator.duplicateItem(item1) expect(duplicate.uuid).to.not.equal(item1.uuid) expect(duplicate.content.references.length).to.equal(1) @@ -223,11 +223,11 @@ describe('app models', () => { it('removing references should update cross-refs', async function () { const item1 = await Factory.createMappedNote(this.application) const item2 = await Factory.createMappedNote(this.application) - const refreshedItem1 = await this.application.itemManager.changeItem(item1, (mutator) => { + const refreshedItem1 = await this.application.mutator.changeItem(item1, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(item2) }) - const refreshedItem1_2 = await this.application.itemManager.emitItemFromPayload( + const refreshedItem1_2 = await this.application.mutator.emitItemFromPayload( refreshedItem1.payloadRepresentation({ deleted: true, content: { @@ -247,7 +247,7 @@ describe('app models', () => { const item1 = await Factory.createMappedNote(this.application) const item2 = await Factory.createMappedNote(this.application) - const refreshedItem1 = await this.application.itemManager.changeItem(item1, (mutator) => { + const refreshedItem1 = await this.application.mutator.changeItem(item1, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(item2) }) @@ -290,12 +290,12 @@ describe('app models', () => { waitingForKey: true, }) - await this.application.itemManager.emitItemFromPayload(errored) + await this.application.payloadManager.emitPayload(errored) expect(this.application.payloadManager.findOne(item1.uuid).errorDecrypting).to.equal(true) expect(this.application.payloadManager.findOne(item1.uuid).items_key_id).to.equal(itemsKey.uuid) - sinon.stub(this.application.protocolService.itemsEncryption, 'decryptErroredPayloads').callsFake(() => { + sinon.stub(this.application.protocolService.itemsEncryption, 'decryptErroredItemPayloads').callsFake(() => { // prevent auto decryption }) @@ -310,7 +310,7 @@ describe('app models', () => { const item2 = await Factory.createMappedNote(this.application) this.expectedItemCount += 2 - await this.application.itemManager.changeItem(item1, (mutator) => { + await this.application.mutator.changeItem(item1, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(item2) }) @@ -339,13 +339,13 @@ describe('app models', () => { it('maintains referencing relationships when duplicating', async function () { const tag = await Factory.createMappedTag(this.application) const note = await Factory.createMappedNote(this.application) - const refreshedTag = await this.application.itemManager.changeItem(tag, (mutator) => { + const refreshedTag = await this.application.mutator.changeItem(tag, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(note) }) expect(refreshedTag.content.references.length).to.equal(1) - const noteCopy = await this.application.itemManager.duplicateItem(note) + const noteCopy = await this.application.mutator.duplicateItem(note) expect(note.uuid).to.not.equal(noteCopy.uuid) expect(this.application.itemManager.getDisplayableNotes().length).to.equal(2) @@ -358,7 +358,7 @@ describe('app models', () => { }) it('maintains editor reference when duplicating note', async function () { - const editor = await this.application.itemManager.createItem( + const editor = await this.application.mutator.createItem( ContentType.Component, { area: ComponentArea.Editor, package_info: { identifier: 'foo-editor' } }, true, @@ -369,7 +369,7 @@ describe('app models', () => { expect(this.application.componentManager.editorForNote(note).uuid).to.equal(editor.uuid) - const duplicate = await this.application.itemManager.duplicateItem(note, true) + const duplicate = await this.application.mutator.duplicateItem(note, true) expect(this.application.componentManager.editorForNote(duplicate).uuid).to.equal(editor.uuid) }) }) diff --git a/packages/snjs/mocha/model_tests/importing.test.js b/packages/snjs/mocha/model_tests/importing.test.js index f23bc4b26..6d374e50f 100644 --- a/packages/snjs/mocha/model_tests/importing.test.js +++ b/packages/snjs/mocha/model_tests/importing.test.js @@ -1,6 +1,6 @@ /* eslint-disable no-unused-expressions */ /* eslint-disable no-undef */ -import { BaseItemCounts } from '../lib/Applications.js' +import { BaseItemCounts } from '../lib/BaseItemCounts.js' import * as Factory from '../lib/factory.js' import { createRelatedNoteTagPairPayload } from '../lib/Items.js' chai.use(chaiAsPromised) @@ -43,7 +43,7 @@ describe('importing', function () { it('should not import backups made from unsupported versions', async function () { await setup({ fakeCrypto: true }) - const result = await application.mutator.importData({ + const result = await application.importData({ version: '-1', items: [], }) @@ -58,7 +58,7 @@ describe('importing', function () { password, version: ProtocolVersion.V003, }) - const result = await application.mutator.importData({ + const result = await application.importData({ version: ProtocolVersion.V004, items: [], }) @@ -71,7 +71,7 @@ describe('importing', function () { const notePayload = pair[0] const tagPayload = pair[1] - await application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged) + await application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged) expectedItemCount += 2 const note = application.itemManager.getItems([ContentType.Note])[0] const tag = application.itemManager.getItems([ContentType.Tag])[0] @@ -82,7 +82,7 @@ describe('importing', function () { expect(note.content.references.length).to.equal(0) expect(application.itemManager.itemsReferencingItem(note).length).to.equal(1) - await application.mutator.importData( + await application.importData( { items: [notePayload, tagPayload], }, @@ -105,7 +105,7 @@ describe('importing', function () { */ await setup({ fakeCrypto: true }) const notePayload = Factory.createNotePayload() - await application.itemManager.emitItemFromPayload(notePayload, PayloadEmitSource.LocalChanged) + await application.mutator.emitItemFromPayload(notePayload, PayloadEmitSource.LocalChanged) expectedItemCount++ const mutatedNote = new DecryptedPayload({ ...notePayload, @@ -114,7 +114,7 @@ describe('importing', function () { title: `${Math.random()}`, }, }) - await application.mutator.importData( + await application.importData( { items: [mutatedNote, mutatedNote, mutatedNote], }, @@ -130,7 +130,7 @@ describe('importing', function () { await setup({ fakeCrypto: true }) const pair = createRelatedNoteTagPairPayload() const tagPayload = pair[1] - await application.itemManager.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged) + await application.mutator.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged) const mutatedTag = new DecryptedPayload({ ...tagPayload, content: { @@ -138,7 +138,7 @@ describe('importing', function () { references: [], }, }) - await application.mutator.importData( + await application.importData( { items: [mutatedTag], }, @@ -153,7 +153,7 @@ describe('importing', function () { const pair = createRelatedNoteTagPairPayload() const notePayload = pair[0] const tagPayload = pair[1] - await application.itemManager.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged) + await application.mutator.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged) expectedItemCount += 2 const note = application.itemManager.getDisplayableNotes()[0] const tag = application.itemManager.getDisplayableTags()[0] @@ -171,7 +171,7 @@ describe('importing', function () { title: `${Math.random()}`, }, }) - await application.mutator.importData( + await application.importData( { items: [mutatedNote, mutatedTag], }, @@ -217,7 +217,7 @@ describe('importing', function () { const tag = await Factory.createMappedTag(application) expectedItemCount += 2 - await application.itemManager.changeItem(tag, (mutator) => { + await application.mutator.changeItem(tag, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(note) }) @@ -240,7 +240,7 @@ describe('importing', function () { }, ) - await application.mutator.importData( + await application.importData( { items: [externalNote, externalTag], }, @@ -272,12 +272,14 @@ describe('importing', function () { await application.sync.sync({ awaitAll: true }) await application.mutator.deleteItem(note) + await application.sync.sync() expect(application.items.findItem(note.uuid)).to.not.exist await application.mutator.deleteItem(tag) + await application.sync.sync() expect(application.items.findItem(tag.uuid)).to.not.exist - await application.mutator.importData( + await application.importData( { items: [note, tag], }, @@ -311,7 +313,7 @@ describe('importing', function () { password: password, }) - await application.mutator.importData( + await application.importData( { items: [note.payload], }, @@ -341,7 +343,7 @@ describe('importing', function () { password: password, }) - await application.mutator.importData( + await application.importData( { items: [note], }, @@ -372,12 +374,14 @@ describe('importing', function () { await application.sync.sync({ awaitAll: true }) await application.mutator.deleteItem(note) + await application.sync.sync() expect(application.items.findItem(note.uuid)).to.not.exist await application.mutator.deleteItem(tag) + await application.sync.sync() expect(application.items.findItem(tag.uuid)).to.not.exist - await application.mutator.importData(backupData, true) + await application.importData(backupData, true) expect(application.itemManager.getDisplayableNotes().length).to.equal(1) expect(application.items.findItem(note.uuid).deleted).to.not.be.ok @@ -402,7 +406,7 @@ describe('importing', function () { application = await Factory.createInitAppWithFakeCrypto() Factory.handlePasswordChallenges(application, password) - await application.mutator.importData(backupData, true) + await application.importData(backupData, true) const importedNote = application.items.findItem(note.uuid) const importedTag = application.items.findItem(tag.uuid) @@ -427,7 +431,7 @@ describe('importing', function () { application = await Factory.createInitAppWithFakeCrypto() Factory.handlePasswordChallenges(application, password) - await application.mutator.importData(backupData, true) + await application.importData(backupData, true) const importedNote = application.items.findItem(note.uuid) const importedTag = application.items.findItem(tag.uuid) @@ -445,7 +449,7 @@ describe('importing', function () { version: oldVersion, }) - const noteItem = await application.itemManager.createItem(ContentType.Note, { + const noteItem = await application.mutator.createItem(ContentType.Note, { title: 'Encrypted note', text: 'On protocol version 003.', }) @@ -456,7 +460,7 @@ describe('importing', function () { application = await Factory.createInitAppWithFakeCrypto() Factory.handlePasswordChallenges(application, password) - const result = await application.mutator.importData(backupData, true) + const result = await application.importData(backupData, true) expect(result).to.not.be.undefined expect(result.affectedItems.length).to.be.eq(backupData.items.length) expect(result.errorCount).to.be.eq(0) @@ -512,7 +516,7 @@ describe('importing', function () { application = await Factory.createInitAppWithRealCrypto() Factory.handlePasswordChallenges(application, password) - const result = await application.mutator.importData(backupData, true) + const result = await application.importData(backupData, true) expect(result).to.not.be.undefined expect(result.affectedItems.length).to.be.eq(backupData.items.length) expect(result.errorCount).to.be.eq(0) @@ -526,7 +530,7 @@ describe('importing', function () { password: password, }) - const noteItem = await application.itemManager.createItem(ContentType.Note, { + const noteItem = await application.mutator.createItem(ContentType.Note, { title: 'Encrypted note', text: 'On protocol version 004.', }) @@ -537,7 +541,7 @@ describe('importing', function () { application = await Factory.createInitAppWithFakeCrypto() Factory.handlePasswordChallenges(application, password) - const result = await application.mutator.importData(backupData, true) + const result = await application.importData(backupData, true) expect(result).to.not.be.undefined expect(result.affectedItems.length).to.be.eq(backupData.items.length) expect(result.errorCount).to.be.eq(0) @@ -556,7 +560,7 @@ describe('importing', function () { password: password, }) - const noteItem = await application.itemManager.createItem(ContentType.Note, { + const noteItem = await application.mutator.createItem(ContentType.Note, { title: 'This is a valid, encrypted note', text: 'On protocol version 004.', }) @@ -577,7 +581,7 @@ describe('importing', function () { backupData.items = [...backupData.items, madeUpPayload] - const result = await application.mutator.importData(backupData, true) + const result = await application.importData(backupData, true) expect(result).to.not.be.undefined expect(result.affectedItems.length).to.be.eq(backupData.items.length - 1) expect(result.errorCount).to.be.eq(1) @@ -594,7 +598,7 @@ describe('importing', function () { version: oldVersion, }) - await application.itemManager.createItem(ContentType.Note, { + await application.mutator.createItem(ContentType.Note, { title: 'Encrypted note', text: 'On protocol version 003.', }) @@ -615,7 +619,7 @@ describe('importing', function () { }, }) - const result = await application.mutator.importData(backupData, true) + const result = await application.importData(backupData, true) expect(result).to.not.be.undefined expect(result.affectedItems.length).to.be.eq(0) @@ -631,7 +635,7 @@ describe('importing', function () { password: password, }) - await application.itemManager.createItem(ContentType.Note, { + await application.mutator.createItem(ContentType.Note, { title: 'This is a valid, encrypted note', text: 'On protocol version 004.', }) @@ -647,7 +651,7 @@ describe('importing', function () { }, }) - const result = await application.mutator.importData(backupData, true) + const result = await application.importData(backupData, true) expect(result).to.not.be.undefined expect(result.affectedItems.length).to.be.eq(0) expect(result.errorCount).to.be.eq(backupData.items.length) @@ -662,7 +666,7 @@ describe('importing', function () { password: password, }) - await application.itemManager.createItem(ContentType.Note, { + await application.mutator.createItem(ContentType.Note, { title: 'Encrypted note', text: 'On protocol version 004.', }) @@ -673,7 +677,7 @@ describe('importing', function () { await Factory.safeDeinit(application) application = await Factory.createInitAppWithFakeCrypto() - const result = await application.mutator.importData(backupData) + const result = await application.importData(backupData) expect(result.error).to.be.ok }) @@ -687,7 +691,7 @@ describe('importing', function () { }) Factory.handlePasswordChallenges(application, password) - await application.itemManager.createItem(ContentType.Note, { + await application.mutator.createItem(ContentType.Note, { title: 'Encrypted note', text: 'On protocol version 004.', }) @@ -699,11 +703,13 @@ describe('importing', function () { application = await Factory.createInitAppWithFakeCrypto() Factory.handlePasswordChallenges(application, password) - const result = await application.mutator.importData(backupData, true) + const result = await application.importData(backupData, true) expect(result).to.not.be.undefined - expect(result.affectedItems.length).to.be.eq(0) - expect(result.errorCount).to.be.eq(backupData.items.length) + + expect(result.affectedItems.length).to.equal(BaseItemCounts.BackupFileRootKeyEncryptedItems) + + expect(result.errorCount).to.be.eq(backupData.items.length - BaseItemCounts.BackupFileRootKeyEncryptedItems) expect(application.itemManager.getDisplayableNotes().length).to.equal(0) }) @@ -784,7 +790,14 @@ describe('importing', function () { await application.prepareForLaunch({ receiveChallenge: (challenge) => { - if (challenge.prompts.length === 2) { + if (challenge.reason === ChallengeReason.Custom) { + return + } + + if ( + challenge.reason === ChallengeReason.DecryptEncryptedFile || + challenge.reason === ChallengeReason.ImportFile + ) { application.submitValuesForChallenge( challenge, challenge.prompts.map((prompt) => @@ -796,9 +809,6 @@ describe('importing', function () { ), ), ) - } else { - const prompt = challenge.prompts[0] - application.submitValuesForChallenge(challenge, [CreateChallengeValue(prompt, password)]) } }, }) @@ -827,7 +837,7 @@ describe('importing', function () { }, } - const result = await application.mutator.importData(backupFile, false) + const result = await application.importData(backupFile, false) expect(result.errorCount).to.equal(0) await Factory.safeDeinit(application) }) @@ -846,7 +856,7 @@ describe('importing', function () { Factory.handlePasswordChallenges(application, password) const pair = createRelatedNoteTagPairPayload() - await application.itemManager.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged) + await application.mutator.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged) await application.sync.sync() @@ -862,7 +872,7 @@ describe('importing', function () { password: password, }) - await application.mutator.importData(backupData, true) + await application.importData(backupData, true) expect(application.itemManager.getDisplayableNotes().length).to.equal(1) expect(application.itemManager.getDisplayableTags().length).to.equal(1) @@ -872,4 +882,8 @@ describe('importing', function () { expect(application.itemManager.referencesForItem(importedTag).length).to.equal(1) expect(application.itemManager.itemsReferencingItem(importedNote).length).to.equal(1) }) + + it('should decrypt backup file which contains a vaulted note without a synced key system root key', async () => { + console.error('TODO: Implement this test') + }) }) diff --git a/packages/snjs/mocha/model_tests/items.test.js b/packages/snjs/mocha/model_tests/items.test.js index c79619ba9..8d7ac26ce 100644 --- a/packages/snjs/mocha/model_tests/items.test.js +++ b/packages/snjs/mocha/model_tests/items.test.js @@ -1,6 +1,6 @@ /* eslint-disable no-unused-expressions */ /* eslint-disable no-undef */ -import { BaseItemCounts } from '../lib/Applications.js' +import { BaseItemCounts } from '../lib/BaseItemCounts.js' import * as Factory from '../lib/factory.js' chai.use(chaiAsPromised) const expect = chai.expect @@ -22,11 +22,11 @@ describe('items', () => { it('setting an item as dirty should update its client updated at', async function () { const params = Factory.createNotePayload() - await this.application.itemManager.emitItemsFromPayloads([params], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([params], PayloadEmitSource.LocalChanged) const item = this.application.itemManager.items[0] const prevDate = item.userModifiedDate.getTime() await Factory.sleep(0.1) - await this.application.itemManager.setItemDirty(item, true) + await this.application.mutator.setItemDirty(item, true) const refreshedItem = this.application.itemManager.findItem(item.uuid) const newDate = refreshedItem.userModifiedDate.getTime() expect(prevDate).to.not.equal(newDate) @@ -34,23 +34,23 @@ describe('items', () => { it('setting an item as dirty with option to skip client updated at', async function () { const params = Factory.createNotePayload() - await this.application.itemManager.emitItemsFromPayloads([params], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([params], PayloadEmitSource.LocalChanged) const item = this.application.itemManager.items[0] const prevDate = item.userModifiedDate.getTime() await Factory.sleep(0.1) - await this.application.itemManager.setItemDirty(item) + await this.application.mutator.setItemDirty(item) const newDate = item.userModifiedDate.getTime() expect(prevDate).to.equal(newDate) }) it('properly pins, archives, and locks', async function () { const params = Factory.createNotePayload() - await this.application.itemManager.emitItemsFromPayloads([params], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([params], PayloadEmitSource.LocalChanged) const item = this.application.itemManager.items[0] expect(item.pinned).to.not.be.ok - const refreshedItem = await this.application.mutator.changeAndSaveItem( + const refreshedItem = await this.application.changeAndSaveItem( item, (mutator) => { mutator.pinned = true @@ -69,7 +69,7 @@ describe('items', () => { it('properly compares item equality', async function () { const params1 = Factory.createNotePayload() const params2 = Factory.createNotePayload() - await this.application.itemManager.emitItemsFromPayloads([params1, params2], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([params1, params2], PayloadEmitSource.LocalChanged) let item1 = this.application.itemManager.getDisplayableNotes()[0] let item2 = this.application.itemManager.getDisplayableNotes()[1] @@ -77,7 +77,7 @@ describe('items', () => { expect(item1.isItemContentEqualWith(item2)).to.equal(true) // items should ignore this field when checking for equality - item1 = await this.application.mutator.changeAndSaveItem( + item1 = await this.application.changeAndSaveItem( item1, (mutator) => { mutator.userModifiedDate = new Date() @@ -86,7 +86,7 @@ describe('items', () => { undefined, syncOptions, ) - item2 = await this.application.mutator.changeAndSaveItem( + item2 = await this.application.changeAndSaveItem( item2, (mutator) => { mutator.userModifiedDate = undefined @@ -98,7 +98,7 @@ describe('items', () => { expect(item1.isItemContentEqualWith(item2)).to.equal(true) - item1 = await this.application.mutator.changeAndSaveItem( + item1 = await this.application.changeAndSaveItem( item1, (mutator) => { mutator.mutableContent.foo = 'bar' @@ -110,7 +110,7 @@ describe('items', () => { expect(item1.isItemContentEqualWith(item2)).to.equal(false) - item2 = await this.application.mutator.changeAndSaveItem( + item2 = await this.application.changeAndSaveItem( item2, (mutator) => { mutator.mutableContent.foo = 'bar' @@ -123,7 +123,7 @@ describe('items', () => { expect(item1.isItemContentEqualWith(item2)).to.equal(true) expect(item2.isItemContentEqualWith(item1)).to.equal(true) - item1 = await this.application.mutator.changeAndSaveItem( + item1 = await this.application.changeAndSaveItem( item1, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(item2) @@ -132,7 +132,7 @@ describe('items', () => { undefined, syncOptions, ) - item2 = await this.application.mutator.changeAndSaveItem( + item2 = await this.application.changeAndSaveItem( item2, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(item1) @@ -147,7 +147,7 @@ describe('items', () => { expect(item1.isItemContentEqualWith(item2)).to.equal(false) - item1 = await this.application.mutator.changeAndSaveItem( + item1 = await this.application.changeAndSaveItem( item1, (mutator) => { mutator.removeItemAsRelationship(item2) @@ -156,7 +156,7 @@ describe('items', () => { undefined, syncOptions, ) - item2 = await this.application.mutator.changeAndSaveItem( + item2 = await this.application.changeAndSaveItem( item2, (mutator) => { mutator.removeItemAsRelationship(item1) @@ -174,12 +174,12 @@ describe('items', () => { it('content equality should not have side effects', async function () { const params1 = Factory.createNotePayload() const params2 = Factory.createNotePayload() - await this.application.itemManager.emitItemsFromPayloads([params1, params2], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([params1, params2], PayloadEmitSource.LocalChanged) let item1 = this.application.itemManager.getDisplayableNotes()[0] const item2 = this.application.itemManager.getDisplayableNotes()[1] - item1 = await this.application.mutator.changeAndSaveItem( + item1 = await this.application.changeAndSaveItem( item1, (mutator) => { mutator.mutableContent.foo = 'bar' @@ -203,7 +203,7 @@ describe('items', () => { // There was an issue where calling that function would modify values directly to omit keys // in contentKeysToIgnoreWhenCheckingEquality. - await this.application.itemManager.setItemsDirty([item1, item2]) + await this.application.mutator.setItemsDirty([item1, item2]) expect(item1.userModifiedDate).to.be.ok expect(item2.userModifiedDate).to.be.ok diff --git a/packages/snjs/mocha/model_tests/mapping.test.js b/packages/snjs/mocha/model_tests/mapping.test.js index 791c04adb..06ede1a7a 100644 --- a/packages/snjs/mocha/model_tests/mapping.test.js +++ b/packages/snjs/mocha/model_tests/mapping.test.js @@ -1,6 +1,6 @@ /* eslint-disable no-unused-expressions */ /* eslint-disable no-undef */ -import { BaseItemCounts } from '../lib/Applications.js' +import { BaseItemCounts } from '../lib/BaseItemCounts.js' import * as Factory from '../lib/factory.js' import { createNoteParams } from '../lib/Items.js' chai.use(chaiAsPromised) @@ -20,7 +20,7 @@ describe('model manager mapping', () => { it('mapping nonexistent item creates it', async function () { const payload = Factory.createNotePayload() - await this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged) this.expectedItemCount++ expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount) }) @@ -31,13 +31,13 @@ describe('model manager mapping', () => { dirty: false, deleted: true, }) - await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) + await this.application.payloadManager.emitPayload(payload, PayloadEmitSource.LocalChanged) expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount) }) it('mapping and deleting nonexistent item creates and deletes it', async function () { const payload = Factory.createNotePayload() - await this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged) this.expectedItemCount++ @@ -51,7 +51,7 @@ describe('model manager mapping', () => { this.expectedItemCount-- - await this.application.itemManager.emitItemsFromPayloads([changedParams], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([changedParams], PayloadEmitSource.LocalChanged) expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount) }) @@ -59,22 +59,22 @@ describe('model manager mapping', () => { it('mapping deleted but dirty item should not delete it', async function () { const payload = Factory.createNotePayload() - const [item] = await this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged) + const [item] = await this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged) this.expectedItemCount++ - await this.application.itemManager.emitItemFromPayload(new DeleteItemMutator(item).getDeletedResult()) + await this.application.payloadManager.emitPayload(new DeleteItemMutator(item).getDeletedResult()) const payload2 = new DeletedPayload(this.application.payloadManager.findOne(payload.uuid).ejected()) - await this.application.itemManager.emitItemsFromPayloads([payload2], PayloadEmitSource.LocalChanged) + await this.application.payloadManager.emitPayloads([payload2], PayloadEmitSource.LocalChanged) expect(this.application.payloadManager.collection.all().length).to.equal(this.expectedItemCount) }) it('mapping existing item updates its properties', async function () { const payload = Factory.createNotePayload() - await this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged) const newTitle = 'updated title' const mutated = new DecryptedPayload({ @@ -84,7 +84,7 @@ describe('model manager mapping', () => { title: newTitle, }, }) - await this.application.itemManager.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([mutated], PayloadEmitSource.LocalChanged) const item = this.application.itemManager.getDisplayableNotes()[0] expect(item.content.title).to.equal(newTitle) @@ -92,9 +92,9 @@ describe('model manager mapping', () => { it('setting an item dirty should retrieve it in dirty items', async function () { const payload = Factory.createNotePayload() - await this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged) const note = this.application.itemManager.getDisplayableNotes()[0] - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) const dirtyItems = this.application.itemManager.getDirtyItems() expect(Uuids(dirtyItems).includes(note.uuid)) }) @@ -106,7 +106,7 @@ describe('model manager mapping', () => { for (let i = 0; i < count; i++) { payloads.push(Factory.createNotePayload()) } - await this.application.itemManager.emitItemsFromPayloads(payloads, PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads(payloads, PayloadEmitSource.LocalChanged) await this.application.syncService.markAllItemsAsNeedingSyncAndPersist() const dirtyItems = this.application.itemManager.getDirtyItems() @@ -115,14 +115,14 @@ describe('model manager mapping', () => { it('sync observers should be notified of changes', async function () { const payload = Factory.createNotePayload() - await this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged) const item = this.application.itemManager.items[0] return new Promise((resolve) => { this.application.itemManager.addObserver(ContentType.Any, ({ changed }) => { expect(changed[0].uuid === item.uuid) resolve() }) - this.application.itemManager.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged) + this.application.mutator.emitItemsFromPayloads([payload], PayloadEmitSource.LocalChanged) }) }) }) diff --git a/packages/snjs/mocha/model_tests/notes_tags.test.js b/packages/snjs/mocha/model_tests/notes_tags.test.js index 2ebe3d37a..5936e8992 100644 --- a/packages/snjs/mocha/model_tests/notes_tags.test.js +++ b/packages/snjs/mocha/model_tests/notes_tags.test.js @@ -2,7 +2,7 @@ import * as Factory from '../lib/factory.js' import * as Utils from '../lib/Utils.js' import { createRelatedNoteTagPairPayload } from '../lib/Items.js' -import { BaseItemCounts } from '../lib/Applications.js' +import { BaseItemCounts } from '../lib/BaseItemCounts.js' chai.use(chaiAsPromised) const expect = chai.expect @@ -25,7 +25,7 @@ describe('notes and tags', () => { it('uses proper class for note', async function () { const payload = Factory.createNotePayload() - await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) const note = this.application.itemManager.getItems([ContentType.Note])[0] expect(note.constructor === SNNote).to.equal(true) }) @@ -33,7 +33,7 @@ describe('notes and tags', () => { it('properly constructs syncing params', async function () { const title = 'Foo' const text = 'Bar' - const note = await this.application.mutator.createTemplateItem(ContentType.Note, { + const note = await this.application.items.createTemplateItem(ContentType.Note, { title, text, }) @@ -41,7 +41,7 @@ describe('notes and tags', () => { expect(note.content.title).to.equal(title) expect(note.content.text).to.equal(text) - const tag = await this.application.mutator.createTemplateItem(ContentType.Tag, { + const tag = await this.application.items.createTemplateItem(ContentType.Tag, { title, }) @@ -73,7 +73,7 @@ describe('notes and tags', () => { }, }) - await this.application.itemManager.emitItemsFromPayloads([mutatedNote, mutatedTag], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([mutatedNote, mutatedTag], PayloadEmitSource.LocalChanged) const note = this.application.itemManager.getItems([ContentType.Note])[0] const tag = this.application.itemManager.getItems([ContentType.Tag])[0] @@ -89,7 +89,7 @@ describe('notes and tags', () => { expect(notePayload.content.references.length).to.equal(0) expect(tagPayload.content.references.length).to.equal(1) - await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged) let note = this.application.itemManager.getDisplayableNotes()[0] let tag = this.application.itemManager.getDisplayableTags()[0] @@ -106,7 +106,7 @@ describe('notes and tags', () => { expect(note.payload.references.length).to.equal(0) expect(tag.noteCount).to.equal(1) - await this.application.itemManager.setItemToBeDeleted(note) + await this.application.mutator.setItemToBeDeleted(note) tag = this.application.itemManager.getDisplayableTags()[0] @@ -130,7 +130,7 @@ describe('notes and tags', () => { const notePayload = pair[0] const tagPayload = pair[1] - await this.application.itemManager.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged) let note = this.application.itemManager.getItems([ContentType.Note])[0] let tag = this.application.itemManager.getItems([ContentType.Tag])[0] @@ -147,7 +147,7 @@ describe('notes and tags', () => { references: [], }, }) - await this.application.itemManager.emitItemsFromPayloads([mutatedTag], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([mutatedTag], PayloadEmitSource.LocalChanged) note = this.application.itemManager.findItem(note.uuid) tag = this.application.itemManager.findItem(tag.uuid) @@ -177,14 +177,14 @@ describe('notes and tags', () => { const notePayload = pair[0] const tagPayload = pair[1] - await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged) const note = this.application.itemManager.getItems([ContentType.Note])[0] let tag = this.application.itemManager.getItems([ContentType.Tag])[0] expect(note.content.references.length).to.equal(0) expect(tag.content.references.length).to.equal(1) - tag = await this.application.mutator.changeAndSaveItem( + tag = await this.application.changeAndSaveItem( tag, (mutator) => { mutator.removeItemAsRelationship(note) @@ -200,11 +200,11 @@ describe('notes and tags', () => { it('properly handles tag duplication', async function () { const pair = createRelatedNoteTagPairPayload() - await this.application.itemManager.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads(pair, PayloadEmitSource.LocalChanged) let note = this.application.itemManager.getDisplayableNotes()[0] let tag = this.application.itemManager.getDisplayableTags()[0] - const duplicateTag = await this.application.itemManager.duplicateItem(tag, true) + const duplicateTag = await this.application.mutator.duplicateItem(tag, true) await this.application.syncService.sync(syncOptions) note = this.application.itemManager.findItem(note.uuid) @@ -232,9 +232,9 @@ describe('notes and tags', () => { const pair = createRelatedNoteTagPairPayload() const notePayload = pair[0] const tagPayload = pair[1] - await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged) const note = this.application.itemManager.getItems([ContentType.Note])[0] - const duplicateNote = await this.application.itemManager.duplicateItem(note, true) + const duplicateNote = await this.application.mutator.duplicateItem(note, true) expect(note.uuid).to.not.equal(duplicateNote.uuid) expect(this.application.itemManager.itemsReferencingItem(duplicateNote).length).to.equal( @@ -246,7 +246,7 @@ describe('notes and tags', () => { const pair = createRelatedNoteTagPairPayload() const notePayload = pair[0] const tagPayload = pair[1] - await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged) const note = this.application.itemManager.getItems([ContentType.Note])[0] let tag = this.application.itemManager.getItems([ContentType.Tag])[0] @@ -256,16 +256,16 @@ describe('notes and tags', () => { expect(note.content.references.length).to.equal(0) expect(this.application.itemManager.itemsReferencingItem(note).length).to.equal(1) - await this.application.itemManager.setItemToBeDeleted(tag) + await this.application.mutator.setItemToBeDeleted(tag) tag = this.application.itemManager.findItem(tag.uuid) expect(tag).to.not.be.ok }) it('modifying item content should not modify payload content', async function () { const notePayload = Factory.createNotePayload() - await this.application.itemManager.emitItemsFromPayloads([notePayload], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([notePayload], PayloadEmitSource.LocalChanged) let note = this.application.itemManager.getItems([ContentType.Note])[0] - note = await this.application.mutator.changeAndSaveItem( + note = await this.application.changeAndSaveItem( note, (mutator) => { mutator.mutableContent.title = Math.random() @@ -285,12 +285,12 @@ describe('notes and tags', () => { const notePayload = pair[0] const tagPayload = pair[1] - await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged) let note = this.application.itemManager.getItems([ContentType.Note])[0] let tag = this.application.itemManager.getItems([ContentType.Tag])[0] await this.application.syncService.sync(syncOptions) - await this.application.itemManager.setItemToBeDeleted(tag) + await this.application.mutator.setItemToBeDeleted(tag) note = this.application.itemManager.findItem(note.uuid) this.application.itemManager.findItem(tag.uuid) @@ -302,7 +302,7 @@ describe('notes and tags', () => { await Promise.all( ['Y', 'Z', 'A', 'B'].map(async (title) => { return this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { title }), + await this.application.items.createTemplateItem(ContentType.Note, { title }), ) }), ) @@ -316,7 +316,7 @@ describe('notes and tags', () => { }) it('setting a note dirty should collapse its properties into content', async function () { - let note = await this.application.mutator.createTemplateItem(ContentType.Note, { + let note = await this.application.items.createTemplateItem(ContentType.Note, { title: 'Foo', }) await this.application.mutator.insertItem(note) @@ -339,7 +339,7 @@ describe('notes and tags', () => { mutator.e2ePendingRefactor_addItemAsRelationship(taggedNote) }) await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title: 'A', }), ) @@ -379,7 +379,7 @@ describe('notes and tags', () => { await Promise.all( ['Y', 'Z', 'A', 'B'].map(async (title) => { return this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title, }), ) @@ -413,17 +413,17 @@ describe('notes and tags', () => { describe('Smart views', function () { it('"title", "startsWith", "Foo"', async function () { const note = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title: 'Foo 🎲', }), ) await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title: 'Not Foo 🎲', }), ) const view = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.SmartView, { + await this.application.items.createTemplateItem(ContentType.SmartView, { title: 'Foo Notes', predicate: { keypath: 'title', @@ -447,7 +447,7 @@ describe('notes and tags', () => { it('"pinned", "=", true', async function () { const note = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title: 'A', }), ) @@ -455,13 +455,13 @@ describe('notes and tags', () => { mutator.pinned = true }) await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title: 'B', pinned: false, }), ) const view = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.SmartView, { + await this.application.items.createTemplateItem(ContentType.SmartView, { title: 'Pinned', predicate: { keypath: 'pinned', @@ -485,7 +485,7 @@ describe('notes and tags', () => { it('"pinned", "=", false', async function () { const pinnedNote = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title: 'A', }), ) @@ -493,12 +493,12 @@ describe('notes and tags', () => { mutator.pinned = true }) const unpinnedNote = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title: 'B', }), ) const view = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.SmartView, { + await this.application.items.createTemplateItem(ContentType.SmartView, { title: 'Not pinned', predicate: { keypath: 'pinned', @@ -522,19 +522,19 @@ describe('notes and tags', () => { it('"text.length", ">", 500', async function () { const longNote = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title: 'A', text: Array(501).fill(0).join(''), }), ) await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title: 'B', text: 'b', }), ) const view = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.SmartView, { + await this.application.items.createTemplateItem(ContentType.SmartView, { title: 'Long', predicate: { keypath: 'text.length', @@ -563,18 +563,20 @@ describe('notes and tags', () => { }) const recentNote = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title: 'A', }), + true, ) await this.application.sync.sync() const olderNote = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title: 'B', text: 'b', }), + true, ) const threeDays = 3 * 24 * 60 * 60 * 1000 @@ -582,13 +584,13 @@ describe('notes and tags', () => { /** Create an unsynced note which shouldn't get an updated_at */ await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title: 'B', text: 'b', }), ) const view = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.SmartView, { + await this.application.items.createTemplateItem(ContentType.SmartView, { title: 'One day ago', predicate: { keypath: 'serverUpdatedAt', @@ -598,6 +600,9 @@ describe('notes and tags', () => { }), ) const matches = this.application.items.notesMatchingSmartView(view) + expect(matches.length).to.equal(1) + expect(matches[0].uuid).to.equal(recentNote.uuid) + this.application.items.setPrimaryItemDisplayOptions({ sortBy: 'title', sortDirection: 'asc', @@ -605,13 +610,11 @@ describe('notes and tags', () => { }) const displayedNotes = this.application.items.getDisplayableNotes() expect(displayedNotes).to.deep.equal(matches) - expect(matches.length).to.equal(1) - expect(matches[0].uuid).to.equal(recentNote.uuid) }) it('"tags.length", "=", 0', async function () { const untaggedNote = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title: 'A', }), ) @@ -622,7 +625,7 @@ describe('notes and tags', () => { }) const view = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.SmartView, { + await this.application.items.createTemplateItem(ContentType.SmartView, { title: 'Untagged', predicate: { keypath: 'tags.length', @@ -650,13 +653,13 @@ describe('notes and tags', () => { mutator.e2ePendingRefactor_addItemAsRelationship(taggedNote) }) await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title: 'A', }), ) const view = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.SmartView, { + await this.application.items.createTemplateItem(ContentType.SmartView, { title: 'B-tags', predicate: { keypath: 'tags', @@ -685,7 +688,7 @@ describe('notes and tags', () => { }) const pinnedNote = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title: 'A', }), ) @@ -694,7 +697,7 @@ describe('notes and tags', () => { }) const lockedNote = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title: 'A', }), ) @@ -703,7 +706,7 @@ describe('notes and tags', () => { }) const view = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.SmartView, { + await this.application.items.createTemplateItem(ContentType.SmartView, { title: 'Pinned & Locked', predicate: { operator: 'and', @@ -733,7 +736,7 @@ describe('notes and tags', () => { }) const pinnedNote = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title: 'A', }), ) @@ -742,7 +745,7 @@ describe('notes and tags', () => { }) const pinnedAndProtectedNote = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title: 'A', }), ) @@ -752,13 +755,13 @@ describe('notes and tags', () => { }) await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.Note, { + await this.application.items.createTemplateItem(ContentType.Note, { title: 'A', }), ) const view = await this.application.mutator.insertItem( - await this.application.mutator.createTemplateItem(ContentType.SmartView, { + await this.application.items.createTemplateItem(ContentType.SmartView, { title: 'Protected or Pinned', predicate: { operator: 'or', @@ -794,7 +797,7 @@ describe('notes and tags', () => { const notePayload3 = Factory.createNotePayload('Bar') const notePayload4 = Factory.createNotePayload('Testing') - await this.application.itemManager.emitItemsFromPayloads( + await this.application.mutator.emitItemsFromPayloads( [notePayload1, notePayload2, notePayload3, notePayload4, tagPayload1], PayloadEmitSource.LocalChanged, ) @@ -824,7 +827,7 @@ describe('notes and tags', () => { const notePayload3 = Factory.createNotePayload('Testing FOO (Bar)') const notePayload4 = Factory.createNotePayload('This should not match') - await this.application.itemManager.emitItemsFromPayloads( + await this.application.mutator.emitItemsFromPayloads( [notePayload1, notePayload2, notePayload3, notePayload4, tagPayload1], PayloadEmitSource.LocalChanged, ) diff --git a/packages/snjs/mocha/model_tests/notes_tags_folders.test.js b/packages/snjs/mocha/model_tests/notes_tags_folders.test.js index f63bd8e55..ffa239153 100644 --- a/packages/snjs/mocha/model_tests/notes_tags_folders.test.js +++ b/packages/snjs/mocha/model_tests/notes_tags_folders.test.js @@ -75,8 +75,8 @@ describe('tags as folders', () => { const note2 = await Factory.createMappedNote(this.application, 'my second note') // ## The user add a note to the child tag - await this.application.items.addTagToNote(note1, tags.child, true) - await this.application.items.addTagToNote(note2, tags.another, true) + await this.application.mutator.addTagToNote(note1, tags.child, true) + await this.application.mutator.addTagToNote(note2, tags.another, true) // ## The note has been added to other tags const note1Tags = await this.application.items.getSortedTagsForItem(note1) diff --git a/packages/snjs/mocha/model_tests/performance.test.js b/packages/snjs/mocha/model_tests/performance.test.js index 233cf56e5..8552eb346 100644 --- a/packages/snjs/mocha/model_tests/performance.test.js +++ b/packages/snjs/mocha/model_tests/performance.test.js @@ -56,7 +56,7 @@ describe('mapping performance', () => { const batchSize = 100 for (let i = 0; i < payloads.length; i += batchSize) { const subArray = payloads.slice(currentIndex, currentIndex + batchSize) - await application.itemManager.emitItemsFromPayloads(subArray, PayloadEmitSource.LocalChanged) + await application.mutator.emitItemsFromPayloads(subArray, PayloadEmitSource.LocalChanged) currentIndex += batchSize } @@ -117,7 +117,7 @@ describe('mapping performance', () => { const batchSize = 100 for (let i = 0; i < payloads.length; i += batchSize) { var subArray = payloads.slice(currentIndex, currentIndex + batchSize) - await application.itemManager.emitItemsFromPayloads(subArray, PayloadEmitSource.LocalChanged) + await application.mutator.emitItemsFromPayloads(subArray, PayloadEmitSource.LocalChanged) currentIndex += batchSize } diff --git a/packages/snjs/mocha/mutator.test.js b/packages/snjs/mocha/mutator.test.js index 88b8aeee9..898283127 100644 --- a/packages/snjs/mocha/mutator.test.js +++ b/packages/snjs/mocha/mutator.test.js @@ -4,7 +4,7 @@ import * as Factory from './lib/factory.js' chai.use(chaiAsPromised) const expect = chai.expect -describe('mutator', () => { +describe('item mutator', () => { beforeEach(async function () { this.createBarePayload = () => { return new DecryptedPayload({ diff --git a/packages/snjs/mocha/mutator_service.test.js b/packages/snjs/mocha/mutator_service.test.js new file mode 100644 index 000000000..de993fd0e --- /dev/null +++ b/packages/snjs/mocha/mutator_service.test.js @@ -0,0 +1,271 @@ +/* eslint-disable no-unused-expressions */ +/* eslint-disable no-undef */ +import * as Factory from './lib/factory.js' +import { BaseItemCounts } from './lib/BaseItemCounts.js' +chai.use(chaiAsPromised) +const expect = chai.expect + +describe('mutator service', function () { + this.timeout(Factory.TwentySecondTimeout) + + let context + let application + let mutator + + afterEach(async function () { + await context.deinit() + localStorage.clear() + }) + + beforeEach(async function () { + localStorage.clear() + + context = await Factory.createAppContextWithFakeCrypto() + application = context.application + mutator = application.mutator + + await context.launch() + }) + + const createNote = async () => { + return mutator.createItem(ContentType.Note, { + title: 'hello', + text: 'world', + }) + } + + const createTag = async (notes = []) => { + const references = notes.map((note) => { + return { + uuid: note.uuid, + content_type: note.content_type, + } + }) + return mutator.createItem(ContentType.Tag, { + title: 'thoughts', + references: references, + }) + } + + it('create item', async function () { + const item = await createNote() + + expect(item).to.be.ok + expect(item.title).to.equal('hello') + }) + + it('emitting item through payload and marking dirty should have userModifiedDate', async function () { + const payload = Factory.createNotePayload() + const item = await mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) + const result = await mutator.setItemDirty(item) + const appData = result.payload.content.appData + expect(appData[DecryptedItem.DefaultAppDomain()][AppDataField.UserModifiedDate]).to.be.ok + }) + + it('deleting an item should make it immediately unfindable', async () => { + const note = await context.createSyncedNote() + await mutator.setItemToBeDeleted(note) + const foundNote = application.items.findItem(note.uuid) + expect(foundNote).to.not.be.ok + }) + + it('deleting from reference map', async function () { + const note = await createNote() + const tag = await createTag([note]) + await mutator.setItemToBeDeleted(note) + + expect(application.items.collection.referenceMap.directMap.get(tag.uuid)).to.eql([]) + expect(application.items.collection.referenceMap.inverseMap.get(note.uuid).length).to.equal(0) + }) + + it('deleting referenced item should update referencing item references', async function () { + const note = await createNote() + let tag = await createTag([note]) + await mutator.setItemToBeDeleted(note) + + tag = application.items.findItem(tag.uuid) + expect(tag.content.references.length).to.equal(0) + }) + + it('removing relationship should update reference map', async function () { + const note = await createNote() + const tag = await createTag([note]) + await mutator.changeItem(tag, (mutator) => { + mutator.removeItemAsRelationship(note) + }) + + expect(application.items.collection.referenceMap.directMap.get(tag.uuid)).to.eql([]) + expect(application.items.collection.referenceMap.inverseMap.get(note.uuid)).to.eql([]) + }) + + it('emitting discardable payload should remove it from our collection', async function () { + const note = await createNote() + + const payload = new DeletedPayload({ + ...note.payload.ejected(), + content: undefined, + deleted: true, + dirty: false, + }) + + expect(payload.discardable).to.equal(true) + + await context.payloads.emitPayload(payload) + + expect(application.items.findItem(note.uuid)).to.not.be.ok + }) + + it('change existing item', async function () { + const note = await createNote() + const newTitle = String(Math.random()) + await mutator.changeItem(note, (mutator) => { + mutator.title = newTitle + }) + + const latestVersion = application.items.findItem(note.uuid) + expect(latestVersion.title).to.equal(newTitle) + }) + + it('change non-existant item through uuid should fail', async function () { + const note = await application.items.createTemplateItem(ContentType.Note, { + title: 'hello', + text: 'world', + }) + + const changeFn = async () => { + const newTitle = String(Math.random()) + return mutator.changeItem(note, (mutator) => { + mutator.title = newTitle + }) + } + await Factory.expectThrowsAsync(() => changeFn(), 'Attempting to change non-existant item') + }) + + it('set items dirty', async function () { + const note = await createNote() + await mutator.setItemDirty(note) + + const dirtyItems = application.items.getDirtyItems() + expect(dirtyItems.length).to.equal(1) + expect(dirtyItems[0].uuid).to.equal(note.uuid) + expect(dirtyItems[0].dirty).to.equal(true) + }) + + describe('duplicateItem', async function () { + const sandbox = sinon.createSandbox() + + beforeEach(async function () { + this.emitPayloads = sandbox.spy(application.items.payloadManager, 'emitPayloads') + }) + + afterEach(async function () { + sandbox.restore() + }) + + it('should duplicate the item and set the duplicate_of property', async function () { + const note = await createNote() + await mutator.duplicateItem(note) + sinon.assert.calledTwice(this.emitPayloads) + + const originalNote = application.items.getDisplayableNotes()[0] + const duplicatedNote = application.items.getDisplayableNotes()[1] + + expect(application.items.items.length).to.equal(2 + BaseItemCounts.DefaultItems) + expect(application.items.getDisplayableNotes().length).to.equal(2) + expect(originalNote.uuid).to.not.equal(duplicatedNote.uuid) + expect(originalNote.uuid).to.equal(duplicatedNote.duplicateOf) + expect(originalNote.uuid).to.equal(duplicatedNote.payload.duplicate_of) + expect(duplicatedNote.conflictOf).to.be.undefined + expect(duplicatedNote.payload.content.conflict_of).to.be.undefined + }) + + it('should duplicate the item and set the duplicate_of and conflict_of properties', async function () { + const note = await createNote() + await mutator.duplicateItem(note, true) + sinon.assert.calledTwice(this.emitPayloads) + + const originalNote = application.items.getDisplayableNotes()[0] + const duplicatedNote = application.items.getDisplayableNotes()[1] + + expect(application.items.items.length).to.equal(2 + BaseItemCounts.DefaultItems) + expect(application.items.getDisplayableNotes().length).to.equal(2) + expect(originalNote.uuid).to.not.equal(duplicatedNote.uuid) + expect(originalNote.uuid).to.equal(duplicatedNote.duplicateOf) + expect(originalNote.uuid).to.equal(duplicatedNote.payload.duplicate_of) + expect(originalNote.uuid).to.equal(duplicatedNote.conflictOf) + expect(originalNote.uuid).to.equal(duplicatedNote.payload.content.conflict_of) + }) + + it('duplicate item with relationships', async function () { + const note = await createNote() + const tag = await createTag([note]) + const duplicate = await mutator.duplicateItem(tag) + + expect(duplicate.content.references).to.have.length(1) + expect(application.items.items).to.have.length(3 + BaseItemCounts.DefaultItems) + expect(application.items.getDisplayableTags()).to.have.length(2) + }) + + it('adds duplicated item as a relationship to items referencing it', async function () { + const note = await createNote() + let tag = await createTag([note]) + const duplicateNote = await mutator.duplicateItem(note) + expect(tag.content.references).to.have.length(1) + + tag = application.items.findItem(tag.uuid) + const references = tag.content.references.map((ref) => ref.uuid) + expect(references).to.have.length(2) + expect(references).to.include(note.uuid, duplicateNote.uuid) + }) + + it('duplicates item with additional content', async function () { + const note = await mutator.createItem(ContentType.Note, { + title: 'hello', + text: 'world', + }) + const duplicateNote = await mutator.duplicateItem(note, false, { + title: 'hello (copy)', + }) + + expect(duplicateNote.title).to.equal('hello (copy)') + expect(duplicateNote.text).to.equal('world') + }) + }) + + it('set item deleted', async function () { + const note = await createNote() + await mutator.setItemToBeDeleted(note) + + /** Items should never be mutated directly */ + expect(note.deleted).to.not.be.ok + + const latestVersion = context.payloads.findOne(note.uuid) + expect(latestVersion.deleted).to.equal(true) + expect(latestVersion.dirty).to.equal(true) + expect(latestVersion.content).to.not.be.ok + + /** Deleted items do not show up in item manager's public interface */ + expect(application.items.items.length).to.equal(BaseItemCounts.DefaultItems) + expect(application.items.getDisplayableNotes().length).to.equal(0) + }) + + it('should empty trash', async function () { + const note = await createNote() + const versionTwo = await mutator.changeItem(note, (mutator) => { + mutator.trashed = true + }) + + expect(application.items.trashSmartView).to.be.ok + expect(versionTwo.trashed).to.equal(true) + expect(versionTwo.dirty).to.equal(true) + expect(versionTwo.content).to.be.ok + + expect(application.items.items.length).to.equal(1 + BaseItemCounts.DefaultItems) + expect(application.items.trashedItems.length).to.equal(1) + + await application.mutator.emptyTrash() + const versionThree = context.payloads.findOne(note.uuid) + expect(versionThree.deleted).to.equal(true) + expect(application.items.trashedItems.length).to.equal(0) + }) +}) diff --git a/packages/snjs/mocha/note_display_criteria.test.js b/packages/snjs/mocha/note_display_criteria.test.js index 750bef622..dfb384605 100644 --- a/packages/snjs/mocha/note_display_criteria.test.js +++ b/packages/snjs/mocha/note_display_criteria.test.js @@ -6,9 +6,10 @@ describe('note display criteria', function () { beforeEach(async function () { this.payloadManager = new PayloadManager() this.itemManager = new ItemManager(this.payloadManager) + this.mutator = new MutatorService(this.itemManager, this.payloadManager) this.createNote = async (title = 'hello', text = 'world') => { - return this.itemManager.createItem(ContentType.Note, { + return this.mutator.createItem(ContentType.Note, { title: title, text: text, }) @@ -21,7 +22,7 @@ describe('note display criteria', function () { content_type: note.content_type, } }) - return this.itemManager.createItem(ContentType.Tag, { + return this.mutator.createItem(ContentType.Tag, { title: title, references: references, }) @@ -31,138 +32,168 @@ describe('note display criteria', function () { it('includePinned off', async function () { await this.createNote() const pendingPin = await this.createNote() - await this.itemManager.changeItem(pendingPin, (mutator) => { + await this.mutator.changeItem(pendingPin, (mutator) => { mutator.pinned = true }) const criteria = { includePinned: false, } expect( - itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection) - .length, + notesAndFilesMatchingOptions( + criteria, + this.itemManager.collection.all(ContentType.Note), + this.itemManager.collection, + ).length, ).to.equal(1) }) it('includePinned on', async function () { await this.createNote() const pendingPin = await this.createNote() - await this.itemManager.changeItem(pendingPin, (mutator) => { + await this.mutator.changeItem(pendingPin, (mutator) => { mutator.pinned = true }) const criteria = { includePinned: true } expect( - itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection) - .length, + notesAndFilesMatchingOptions( + criteria, + this.itemManager.collection.all(ContentType.Note), + this.itemManager.collection, + ).length, ).to.equal(2) }) it('includeTrashed off', async function () { await this.createNote() const pendingTrash = await this.createNote() - await this.itemManager.changeItem(pendingTrash, (mutator) => { + await this.mutator.changeItem(pendingTrash, (mutator) => { mutator.trashed = true }) const criteria = { includeTrashed: false } expect( - itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection) - .length, + notesAndFilesMatchingOptions( + criteria, + this.itemManager.collection.all(ContentType.Note), + this.itemManager.collection, + ).length, ).to.equal(1) }) it('includeTrashed on', async function () { await this.createNote() const pendingTrash = await this.createNote() - await this.itemManager.changeItem(pendingTrash, (mutator) => { + await this.mutator.changeItem(pendingTrash, (mutator) => { mutator.trashed = true }) const criteria = { includeTrashed: true } expect( - itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection) - .length, + notesAndFilesMatchingOptions( + criteria, + this.itemManager.collection.all(ContentType.Note), + this.itemManager.collection, + ).length, ).to.equal(2) }) it('includeArchived off', async function () { await this.createNote() const pendingArchive = await this.createNote() - await this.itemManager.changeItem(pendingArchive, (mutator) => { + await this.mutator.changeItem(pendingArchive, (mutator) => { mutator.archived = true }) const criteria = { includeArchived: false } expect( - itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection) - .length, + notesAndFilesMatchingOptions( + criteria, + this.itemManager.collection.all(ContentType.Note), + this.itemManager.collection, + ).length, ).to.equal(1) }) it('includeArchived on', async function () { await this.createNote() const pendingArchive = await this.createNote() - await this.itemManager.changeItem(pendingArchive, (mutator) => { + await this.mutator.changeItem(pendingArchive, (mutator) => { mutator.archived = true }) const criteria = { includeArchived: true, } expect( - itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection) - .length, + notesAndFilesMatchingOptions( + criteria, + this.itemManager.collection.all(ContentType.Note), + this.itemManager.collection, + ).length, ).to.equal(2) }) it('includeProtected off', async function () { await this.createNote() const pendingProtected = await this.createNote() - await this.itemManager.changeItem(pendingProtected, (mutator) => { + await this.mutator.changeItem(pendingProtected, (mutator) => { mutator.protected = true }) const criteria = { includeProtected: false } expect( - itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection) - .length, + notesAndFilesMatchingOptions( + criteria, + this.itemManager.collection.all(ContentType.Note), + this.itemManager.collection, + ).length, ).to.equal(1) }) it('includeProtected on', async function () { await this.createNote() const pendingProtected = await this.createNote() - await this.itemManager.changeItem(pendingProtected, (mutator) => { + await this.mutator.changeItem(pendingProtected, (mutator) => { mutator.protected = true }) const criteria = { includeProtected: true, } expect( - itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection) - .length, + notesAndFilesMatchingOptions( + criteria, + this.itemManager.collection.all(ContentType.Note), + this.itemManager.collection, + ).length, ).to.equal(2) }) it('protectedSearchEnabled false', async function () { const normal = await this.createNote('hello', 'world') - await this.itemManager.changeItem(normal, (mutator) => { + await this.mutator.changeItem(normal, (mutator) => { mutator.protected = true }) const criteria = { searchQuery: { query: 'world', includeProtectedNoteText: false }, } expect( - itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection) - .length, + notesAndFilesMatchingOptions( + criteria, + this.itemManager.collection.all(ContentType.Note), + this.itemManager.collection, + ).length, ).to.equal(0) }) it('protectedSearchEnabled true', async function () { const normal = await this.createNote() - await this.itemManager.changeItem(normal, (mutator) => { + await this.mutator.changeItem(normal, (mutator) => { mutator.protected = true }) const criteria = { searchQuery: { query: 'world', includeProtectedNoteText: true }, } expect( - itemsMatchingOptions(criteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection) - .length, + notesAndFilesMatchingOptions( + criteria, + this.itemManager.collection.all(ContentType.Note), + this.itemManager.collection, + ).length, ).to.equal(1) }) @@ -175,7 +206,7 @@ describe('note display criteria', function () { tags: [tag], } expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( matchingCriteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection, @@ -186,7 +217,7 @@ describe('note display criteria', function () { tags: [looseTag], } expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( nonmatchingCriteria, this.itemManager.collection.all(ContentType.Note), this.itemManager.collection, @@ -198,7 +229,7 @@ describe('note display criteria', function () { it('normal note', async function () { await this.createNote() expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.allNotesSmartView], }, @@ -208,7 +239,7 @@ describe('note display criteria', function () { ).to.equal(1) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.trashSmartView], }, @@ -218,7 +249,7 @@ describe('note display criteria', function () { ).to.equal(0) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.archivedSmartView], }, @@ -230,12 +261,12 @@ describe('note display criteria', function () { it('trashed note', async function () { const normal = await this.createNote() - await this.itemManager.changeItem(normal, (mutator) => { + await this.mutator.changeItem(normal, (mutator) => { mutator.trashed = true }) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.allNotesSmartView], includeTrashed: false, @@ -246,7 +277,7 @@ describe('note display criteria', function () { ).to.equal(0) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.trashSmartView], }, @@ -256,7 +287,7 @@ describe('note display criteria', function () { ).to.equal(1) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.archivedSmartView], }, @@ -268,12 +299,12 @@ describe('note display criteria', function () { it('archived note', async function () { const normal = await this.createNote() - await this.itemManager.changeItem(normal, (mutator) => { + await this.mutator.changeItem(normal, (mutator) => { mutator.trashed = false mutator.archived = true }) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.allNotesSmartView], includeArchived: false, @@ -284,7 +315,7 @@ describe('note display criteria', function () { ).to.equal(0) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.trashSmartView], }, @@ -294,7 +325,7 @@ describe('note display criteria', function () { ).to.equal(0) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.archivedSmartView], }, @@ -306,13 +337,13 @@ describe('note display criteria', function () { it('archived + trashed note', async function () { const normal = await this.createNote() - await this.itemManager.changeItem(normal, (mutator) => { + await this.mutator.changeItem(normal, (mutator) => { mutator.trashed = true mutator.archived = true }) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.allNotesSmartView], }, @@ -322,7 +353,7 @@ describe('note display criteria', function () { ).to.equal(1) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.trashSmartView], }, @@ -332,7 +363,7 @@ describe('note display criteria', function () { ).to.equal(1) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.archivedSmartView], }, @@ -348,7 +379,7 @@ describe('note display criteria', function () { await this.createNote() expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.allNotesSmartView], includeTrashed: true, @@ -359,7 +390,7 @@ describe('note display criteria', function () { ).to.equal(1) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.trashSmartView], includeTrashed: true, @@ -373,12 +404,12 @@ describe('note display criteria', function () { it('trashed note', async function () { const normal = await this.createNote() - await this.itemManager.changeItem(normal, (mutator) => { + await this.mutator.changeItem(normal, (mutator) => { mutator.trashed = true }) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.allNotesSmartView], includeTrashed: false, @@ -389,7 +420,7 @@ describe('note display criteria', function () { ).to.equal(0) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.allNotesSmartView], includeTrashed: true, @@ -400,7 +431,7 @@ describe('note display criteria', function () { ).to.equal(1) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.trashSmartView], includeTrashed: true, @@ -411,7 +442,7 @@ describe('note display criteria', function () { ).to.equal(1) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.archivedSmartView], includeTrashed: true, @@ -425,13 +456,13 @@ describe('note display criteria', function () { it('archived + trashed note', async function () { const normal = await this.createNote() - await this.itemManager.changeItem(normal, (mutator) => { + await this.mutator.changeItem(normal, (mutator) => { mutator.trashed = true mutator.archived = true }) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.allNotesSmartView], }, @@ -441,7 +472,7 @@ describe('note display criteria', function () { ).to.equal(1) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.trashSmartView], }, @@ -451,7 +482,7 @@ describe('note display criteria', function () { ).to.equal(1) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.archivedSmartView], }, @@ -467,7 +498,7 @@ describe('note display criteria', function () { await this.createNote() expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.allNotesSmartView], includeArchived: true, @@ -478,7 +509,7 @@ describe('note display criteria', function () { ).to.equal(1) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.trashSmartView], includeArchived: true, @@ -491,12 +522,12 @@ describe('note display criteria', function () { it('archived note', async function () { const normal = await this.createNote() - await this.itemManager.changeItem(normal, (mutator) => { + await this.mutator.changeItem(normal, (mutator) => { mutator.archived = true }) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.allNotesSmartView], includeArchived: false, @@ -507,7 +538,7 @@ describe('note display criteria', function () { ).to.equal(0) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.allNotesSmartView], includeArchived: true, @@ -518,7 +549,7 @@ describe('note display criteria', function () { ).to.equal(1) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.trashSmartView], includeArchived: true, @@ -529,7 +560,7 @@ describe('note display criteria', function () { ).to.equal(0) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.archivedSmartView], includeArchived: false, @@ -542,13 +573,13 @@ describe('note display criteria', function () { it('archived + trashed note', async function () { const normal = await this.createNote() - await this.itemManager.changeItem(normal, (mutator) => { + await this.mutator.changeItem(normal, (mutator) => { mutator.trashed = true mutator.archived = true }) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.allNotesSmartView], includeArchived: true, @@ -559,7 +590,7 @@ describe('note display criteria', function () { ).to.equal(1) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.trashSmartView], includeArchived: true, @@ -570,7 +601,7 @@ describe('note display criteria', function () { ).to.equal(1) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.archivedSmartView], includeArchived: true, @@ -587,7 +618,7 @@ describe('note display criteria', function () { await this.createNote() expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [ this.itemManager.allNotesSmartView, @@ -601,7 +632,7 @@ describe('note display criteria', function () { ).to.equal(1) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.trashSmartView], }, @@ -613,12 +644,12 @@ describe('note display criteria', function () { it('archived note', async function () { const normal = await this.createNote() - await this.itemManager.changeItem(normal, (mutator) => { + await this.mutator.changeItem(normal, (mutator) => { mutator.archived = true }) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.allNotesSmartView], includeArchived: false, @@ -629,7 +660,7 @@ describe('note display criteria', function () { ).to.equal(0) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.allNotesSmartView], includeArchived: true, @@ -640,7 +671,7 @@ describe('note display criteria', function () { ).to.equal(1) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.trashSmartView], includeArchived: true, @@ -651,7 +682,7 @@ describe('note display criteria', function () { ).to.equal(0) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.archivedSmartView], includeArchived: false, @@ -664,13 +695,13 @@ describe('note display criteria', function () { it('archived + trashed note', async function () { const normal = await this.createNote() - await this.itemManager.changeItem(normal, (mutator) => { + await this.mutator.changeItem(normal, (mutator) => { mutator.trashed = true mutator.archived = true }) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.allNotesSmartView], includeArchived: true, @@ -681,7 +712,7 @@ describe('note display criteria', function () { ).to.equal(0) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.trashSmartView], includeArchived: true, @@ -692,7 +723,7 @@ describe('note display criteria', function () { ).to.equal(1) expect( - itemsMatchingOptions( + notesAndFilesMatchingOptions( { views: [this.itemManager.archivedSmartView], includeArchived: true, diff --git a/packages/snjs/mocha/protection.test.js b/packages/snjs/mocha/protection.test.js index afcfbf2f3..d84ededbf 100644 --- a/packages/snjs/mocha/protection.test.js +++ b/packages/snjs/mocha/protection.test.js @@ -48,7 +48,7 @@ describe('protections', function () { }) let note = await Factory.createMappedNote(application) - note = await application.mutator.protectNote(note) + note = await application.protections.protectNote(note) expect(await application.authorizeNoteAccess(note)).to.be.true expect(challengePrompts).to.equal(1) @@ -57,7 +57,7 @@ describe('protections', function () { it('sets `note.protected` to true', async function () { application = await Factory.createInitAppWithFakeCrypto() let note = await Factory.createMappedNote(application) - note = await application.mutator.protectNote(note) + note = await application.protections.protectNote(note) expect(note.protected).to.be.true }) @@ -87,7 +87,7 @@ describe('protections', function () { await application.addPasscode(passcode) let note = await Factory.createMappedNote(application) - note = await application.mutator.protectNote(note) + note = await application.protections.protectNote(note) expect(await application.authorizeNoteAccess(note)).to.be.true expect(challengePrompts).to.equal(1) @@ -120,8 +120,8 @@ describe('protections', function () { await application.addPasscode(passcode) let note = await Factory.createMappedNote(application) const uuid = note.uuid - note = await application.mutator.protectNote(note) - note = await application.mutator.unprotectNote(note) + note = await application.protections.protectNote(note) + note = await application.protections.unprotectNote(note) expect(note.uuid).to.equal(uuid) expect(note.protected).to.equal(false) expect(challengePrompts).to.equal(1) @@ -142,8 +142,8 @@ describe('protections', function () { await application.addPasscode(passcode) let note = await Factory.createMappedNote(application) - note = await application.mutator.protectNote(note) - const result = await application.mutator.unprotectNote(note) + note = await application.protections.protectNote(note) + const result = await application.protections.unprotectNote(note) expect(result).to.be.undefined expect(challengePrompts).to.equal(1) }) @@ -174,7 +174,7 @@ describe('protections', function () { await application.addPasscode(passcode) let note = await Factory.createMappedNote(application) - note = await application.mutator.protectNote(note) + note = await application.protections.protectNote(note) expect(await application.authorizeNoteAccess(note)).to.be.true expect(await application.authorizeNoteAccess(note)).to.be.true @@ -226,7 +226,7 @@ describe('protections', function () { application = await Factory.createInitAppWithFakeCrypto() let note = await Factory.createMappedNote(application) - note = await application.mutator.protectNote(note) + note = await application.protections.protectNote(note) expect(await application.authorizeNoteAccess(note)).to.be.true }) @@ -431,8 +431,8 @@ describe('protections', function () { const NOTE_COUNT = 3 let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT) - notes[0] = await application.mutator.protectNote(notes[0]) - notes[1] = await application.mutator.protectNote(notes[1]) + notes[0] = await application.protections.protectNote(notes[0]) + notes[1] = await application.protections.protectNote(notes[1]) expect(await application.authorizeProtectedActionForNotes(notes, ChallengeReason.SelectProtectedNote)).lengthOf( NOTE_COUNT, @@ -468,8 +468,8 @@ describe('protections', function () { const NOTE_COUNT = 3 let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT) - notes[0] = await application.mutator.protectNote(notes[0]) - notes[1] = await application.mutator.protectNote(notes[1]) + notes[0] = await application.protections.protectNote(notes[0]) + notes[1] = await application.protections.protectNote(notes[1]) expect(await application.authorizeProtectedActionForNotes(notes, ChallengeReason.SelectProtectedNote)).lengthOf( NOTE_COUNT, @@ -493,8 +493,8 @@ describe('protections', function () { const NOTE_COUNT = 3 let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT) - notes[0] = await application.mutator.protectNote(notes[0]) - notes[1] = await application.mutator.protectNote(notes[1]) + notes[0] = await application.protections.protectNote(notes[0]) + notes[1] = await application.protections.protectNote(notes[1]) expect(await application.authorizeProtectedActionForNotes(notes, ChallengeReason.SelectProtectedNote)).lengthOf(1) expect(challengePrompts).to.equal(1) @@ -513,7 +513,7 @@ describe('protections', function () { const NOTE_COUNT = 3 let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT) - notes = await application.mutator.protectNotes(notes) + notes = await application.protections.protectNotes(notes) for (const note of notes) { expect(note.protected).to.be.true @@ -550,8 +550,8 @@ describe('protections', function () { const NOTE_COUNT = 3 let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT) - notes = await application.mutator.protectNotes(notes) - notes = await application.mutator.unprotectNotes(notes) + notes = await application.protections.protectNotes(notes) + notes = await application.protections.unprotectNotes(notes) for (const note of notes) { expect(note.protected).to.be.false @@ -587,8 +587,8 @@ describe('protections', function () { const NOTE_COUNT = 3 let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT) - notes = await application.mutator.protectNotes(notes) - notes = await application.mutator.unprotectNotes(notes) + notes = await application.protections.protectNotes(notes) + notes = await application.protections.unprotectNotes(notes) for (const note of notes) { expect(note.protected).to.be.false @@ -612,8 +612,8 @@ describe('protections', function () { const NOTE_COUNT = 3 let notes = await Factory.createManyMappedNotes(application, NOTE_COUNT) - notes = await application.mutator.protectNotes(notes) - notes = await application.mutator.unprotectNotes(notes) + notes = await application.protections.protectNotes(notes) + notes = await application.protections.unprotectNotes(notes) for (const note of notes) { expect(note.protected).to.be(true) diff --git a/packages/snjs/mocha/session.test.js b/packages/snjs/mocha/session.test.js index b1e9d51c3..e96ffd275 100644 --- a/packages/snjs/mocha/session.test.js +++ b/packages/snjs/mocha/session.test.js @@ -1,6 +1,6 @@ /* eslint-disable no-unused-expressions */ /* eslint-disable no-undef */ -import { BaseItemCounts } from './lib/Applications.js' +import { BaseItemCounts } from './lib/BaseItemCounts.js' import * as Factory from './lib/factory.js' import WebDeviceInterface from './lib/web_device_interface.js' chai.use(chaiAsPromised) diff --git a/packages/snjs/mocha/settings.test.js b/packages/snjs/mocha/settings.test.js index cb8feae08..6e1f89880 100644 --- a/packages/snjs/mocha/settings.test.js +++ b/packages/snjs/mocha/settings.test.js @@ -1,5 +1,6 @@ import * as Factory from './lib/factory.js' import * as Files from './lib/Files.js' +import * as Events from './lib/Events.js' chai.use(chaiAsPromised) const expect = chai.expect @@ -98,26 +99,43 @@ describe('settings service', function () { }) it('reads a nonexistent sensitive setting', async () => { - const setting = await application.settings.getDoesSensitiveSettingExist(SettingName.create(SettingName.NAMES.MfaSecret).getValue()) + const setting = await application.settings.getDoesSensitiveSettingExist( + SettingName.create(SettingName.NAMES.MfaSecret).getValue(), + ) expect(setting).to.equal(false) }) it('creates and reads a sensitive setting', async () => { - await application.settings.updateSetting(SettingName.create(SettingName.NAMES.MfaSecret).getValue(), 'fake_secret', true) - const setting = await application.settings.getDoesSensitiveSettingExist(SettingName.create(SettingName.NAMES.MfaSecret).getValue()) + await application.settings.updateSetting( + SettingName.create(SettingName.NAMES.MfaSecret).getValue(), + 'fake_secret', + true, + ) + const setting = await application.settings.getDoesSensitiveSettingExist( + SettingName.create(SettingName.NAMES.MfaSecret).getValue(), + ) expect(setting).to.equal(true) }) it('creates and lists a sensitive setting', async () => { - await application.settings.updateSetting(SettingName.create(SettingName.NAMES.MfaSecret).getValue(), 'fake_secret', true) - await application.settings.updateSetting(SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue(), MuteFailedBackupsEmailsOption.Muted) + await application.settings.updateSetting( + SettingName.create(SettingName.NAMES.MfaSecret).getValue(), + 'fake_secret', + true, + ) + await application.settings.updateSetting( + SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue(), + MuteFailedBackupsEmailsOption.Muted, + ) const settings = await application.settings.listSettings() - expect(settings.getSettingValue(SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue())).to.eql(MuteFailedBackupsEmailsOption.Muted) + expect(settings.getSettingValue(SettingName.create(SettingName.NAMES.MuteFailedBackupsEmails).getValue())).to.eql( + MuteFailedBackupsEmailsOption.Muted, + ) expect(settings.getSettingValue(SettingName.create(SettingName.NAMES.MfaSecret).getValue())).to.not.be.ok }) it('reads a subscription setting - @paidfeature', async () => { - await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', { + await Events.publishMockedEvent('SUBSCRIPTION_PURCHASED', { userEmail: context.email, subscriptionId: subscriptionId++, subscriptionName: 'PRO_PLAN', @@ -130,19 +148,21 @@ describe('settings service', function () { totalActiveSubscriptionsCount: 1, userRegisteredAt: 1, billingFrequency: 12, - payAmount: 59.00 + payAmount: 59.0, }) await Factory.sleep(2) - const setting = await application.settings.getSubscriptionSetting(SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue()) + const setting = await application.settings.getSubscriptionSetting( + SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(), + ) expect(setting).to.be.a('string') }) it('persist irreplaceable subscription settings between subsequent subscriptions - @paidfeature', async () => { await reInitializeApplicationWithRealCrypto() - await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', { + await Events.publishMockedEvent('SUBSCRIPTION_PURCHASED', { userEmail: context.email, subscriptionId: subscriptionId, subscriptionName: 'PRO_PLAN', @@ -155,7 +175,7 @@ describe('settings service', function () { totalActiveSubscriptionsCount: 1, userRegisteredAt: 1, billingFrequency: 12, - payAmount: 59.00 + payAmount: 59.0, }) await Factory.sleep(1) @@ -166,13 +186,17 @@ describe('settings service', function () { await Factory.sleep(1) - const limitSettingBefore = await application.settings.getSubscriptionSetting(SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue()) + const limitSettingBefore = await application.settings.getSubscriptionSetting( + SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(), + ) expect(limitSettingBefore).to.equal('107374182400') - const usedSettingBefore = await application.settings.getSubscriptionSetting(SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue()) + const usedSettingBefore = await application.settings.getSubscriptionSetting( + SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(), + ) expect(usedSettingBefore).to.equal('196') - await Factory.publishMockedEvent('SUBSCRIPTION_EXPIRED', { + await Events.publishMockedEvent('SUBSCRIPTION_EXPIRED', { userEmail: context.email, subscriptionId: subscriptionId++, subscriptionName: 'PRO_PLAN', @@ -181,11 +205,11 @@ describe('settings service', function () { totalActiveSubscriptionsCount: 1, userExistingSubscriptionsCount: 1, billingFrequency: 12, - payAmount: 59.00 + payAmount: 59.0, }) await Factory.sleep(1) - await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', { + await Events.publishMockedEvent('SUBSCRIPTION_PURCHASED', { userEmail: context.email, subscriptionId: subscriptionId++, subscriptionName: 'PRO_PLAN', @@ -198,14 +222,18 @@ describe('settings service', function () { totalActiveSubscriptionsCount: 2, userRegisteredAt: 1, billingFrequency: 12, - payAmount: 59.00 + payAmount: 59.0, }) await Factory.sleep(1) - const limitSettingAfter = await application.settings.getSubscriptionSetting(SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue()) + const limitSettingAfter = await application.settings.getSubscriptionSetting( + SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(), + ) expect(limitSettingAfter).to.equal(limitSettingBefore) - const usedSettingAfter = await application.settings.getSubscriptionSetting(SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue()) + const usedSettingAfter = await application.settings.getSubscriptionSetting( + SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(), + ) expect(usedSettingAfter).to.equal(usedSettingBefore) }) }) diff --git a/packages/snjs/mocha/singletons.test.js b/packages/snjs/mocha/singletons.test.js index e5a1c8d9c..3bd340ceb 100644 --- a/packages/snjs/mocha/singletons.test.js +++ b/packages/snjs/mocha/singletons.test.js @@ -1,6 +1,6 @@ /* eslint-disable no-unused-expressions */ /* eslint-disable no-undef */ -import { BaseItemCounts } from './lib/Applications.js' +import { BaseItemCounts } from './lib/BaseItemCounts.js' import * as Factory from './lib/factory.js' import WebDeviceInterface from './lib/web_device_interface.js' chai.use(chaiAsPromised) @@ -38,7 +38,9 @@ describe('singletons', function () { this.email = UuidGenerator.GenerateUuid() this.password = UuidGenerator.GenerateUuid() + this.registerUser = async () => { + this.expectedItemCount = BaseItemCounts.DefaultItemsWithAccount await Factory.registerUserToApplication({ application: this.application, email: this.email, @@ -62,7 +64,7 @@ describe('singletons', function () { ]) this.createExtMgr = () => { - return this.application.itemManager.createItem( + return this.application.mutator.createItem( ContentType.Component, { package_info: { @@ -93,11 +95,11 @@ describe('singletons', function () { const prefs2 = createPrefsPayload() const prefs3 = createPrefsPayload() - const items = await this.application.itemManager.emitItemsFromPayloads( + const items = await this.application.mutator.emitItemsFromPayloads( [prefs1, prefs2, prefs3], PayloadEmitSource.LocalChanged, ) - await this.application.itemManager.setItemsDirty(items) + await this.application.mutator.setItemsDirty(items) await this.application.syncService.sync(syncOptions) expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount) }) @@ -192,7 +194,7 @@ describe('singletons', function () { if (!beginCheckingResponse) { return } - if (!didCompleteRelevantSync && eventName === SyncEvent.SingleRoundTripSyncCompleted) { + if (!didCompleteRelevantSync && eventName === SyncEvent.PaginatedSyncRequestCompleted) { didCompleteRelevantSync = true const saved = data.savedPayloads expect(saved.length).to.equal(1) @@ -327,7 +329,7 @@ describe('singletons', function () { it('alternating the uuid of a singleton should return correct result', async function () { const payload = createPrefsPayload() - const item = await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) + const item = await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) await this.application.syncService.sync(syncOptions) const predicate = new Predicate('content_type', '=', item.content_type) let resolvedItem = await this.application.singletonManager.findOrCreateContentTypeSingleton( diff --git a/packages/snjs/mocha/storage.test.js b/packages/snjs/mocha/storage.test.js index 1a3a39bc6..9cd3b091d 100644 --- a/packages/snjs/mocha/storage.test.js +++ b/packages/snjs/mocha/storage.test.js @@ -1,6 +1,6 @@ /* eslint-disable no-unused-expressions */ /* eslint-disable no-undef */ -import { BaseItemCounts } from './lib/Applications.js' +import { BaseItemCounts } from './lib/BaseItemCounts.js' import * as Factory from './lib/factory.js' chai.use(chaiAsPromised) const expect = chai.expect @@ -279,7 +279,7 @@ describe('storage manager', function () { }) await Factory.createSyncedNote(this.application) - expect(await Factory.storagePayloadCount(this.application)).to.equal(BaseItemCounts.DefaultItems + 1) + expect(await Factory.storagePayloadCount(this.application)).to.equal(BaseItemCounts.DefaultItemsWithAccount + 1) this.application = await Factory.signOutApplicationAndReturnNew(this.application) await Factory.sleep(0.1, 'Allow all untrackable singleton syncs to complete') expect(await Factory.storagePayloadCount(this.application)).to.equal(BaseItemCounts.DefaultItems) diff --git a/packages/snjs/mocha/subscriptions.test.js b/packages/snjs/mocha/subscriptions.test.js index d787bafc2..bf3cf396d 100644 --- a/packages/snjs/mocha/subscriptions.test.js +++ b/packages/snjs/mocha/subscriptions.test.js @@ -1,4 +1,5 @@ import * as Factory from './lib/factory.js' +import * as Events from './lib/Events.js' chai.use(chaiAsPromised) const expect = chai.expect @@ -31,7 +32,7 @@ describe('subscriptions', function () { password: context.password, }) - await Factory.publishMockedEvent('SUBSCRIPTION_PURCHASED', { + await Events.publishMockedEvent('SUBSCRIPTION_PURCHASED', { userEmail: context.email, subscriptionId: subscriptionId++, subscriptionName: 'PRO_PLAN', diff --git a/packages/snjs/mocha/sync_tests/conflicting.test.js b/packages/snjs/mocha/sync_tests/conflicting.test.js index 1c5c3817a..202894423 100644 --- a/packages/snjs/mocha/sync_tests/conflicting.test.js +++ b/packages/snjs/mocha/sync_tests/conflicting.test.js @@ -1,5 +1,5 @@ /* eslint-disable no-undef */ -import { BaseItemCounts } from '../lib/Applications.js' +import { BaseItemCounts } from '../lib/BaseItemCounts.js' import * as Factory from '../lib/factory.js' import { createSyncedNoteWithTag } from '../lib/Items.js' import * as Utils from '../lib/Utils.js' @@ -16,7 +16,7 @@ describe('online conflict handling', function () { beforeEach(async function () { localStorage.clear() - this.expectedItemCount = BaseItemCounts.DefaultItems + this.expectedItemCount = BaseItemCounts.DefaultItemsWithAccount this.context = await Factory.createAppContextWithFakeCrypto('AppA') await this.context.launch() @@ -64,7 +64,7 @@ describe('online conflict handling', function () { it('components should not be duplicated under any circumstances', async function () { const payload = createDirtyPayload(ContentType.Component) - const item = await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) + const item = await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) this.expectedItemCount++ @@ -91,7 +91,7 @@ describe('online conflict handling', function () { it('items keys should not be duplicated under any circumstances', async function () { const payload = createDirtyPayload(ContentType.ItemsKey) - const item = await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) + const item = await this.application.mutator.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) this.expectedItemCount++ await this.application.syncService.sync(syncOptions) /** First modify the item without saving so that @@ -118,7 +118,7 @@ describe('online conflict handling', function () { // create an item and sync it const note = await Factory.createMappedNote(this.application) this.expectedItemCount++ - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) await this.application.syncService.sync(syncOptions) const rawPayloads = await this.application.diskStorageService.getAllRawPayloads() @@ -128,11 +128,11 @@ describe('online conflict handling', function () { const dirtyValue = `${Math.random()}` /** Modify nonsense first to get around strategyWhenConflictingWithItem with previousRevision check */ - await this.application.itemManager.changeNote(note, (mutator) => { + await this.application.mutator.changeNote(note, (mutator) => { mutator.title = 'any' }) - await this.application.itemManager.changeNote(note, (mutator) => { + await this.application.mutator.changeNote(note, (mutator) => { // modify this item locally to have differing contents from server mutator.title = dirtyValue // Intentionally don't change updated_at. We want to simulate a chaotic case where @@ -238,7 +238,7 @@ describe('online conflict handling', function () { it('should duplicate item if saving a modified item and clearing our sync token', async function () { let note = await Factory.createMappedNote(this.application) - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) await this.application.syncService.sync(syncOptions) this.expectedItemCount++ @@ -279,11 +279,11 @@ describe('online conflict handling', function () { it('should handle sync conflicts by not duplicating same data', async function () { const note = await Factory.createMappedNote(this.application) this.expectedItemCount++ - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) await this.application.syncService.sync(syncOptions) // keep item as is and set dirty - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) // clear sync token so that all items are retrieved on next sync this.application.syncService.clearSyncPositionTokens() @@ -295,10 +295,10 @@ describe('online conflict handling', function () { it('clearing conflict_of on two clients simultaneously should keep us in sync', async function () { const note = await Factory.createMappedNote(this.application) - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) this.expectedItemCount++ - await this.application.mutator.changeAndSaveItem( + await this.application.changeAndSaveItem( note, (mutator) => { // client A @@ -311,7 +311,7 @@ describe('online conflict handling', function () { // client B await this.application.syncService.clearSyncPositionTokens() - await this.application.itemManager.changeItem( + await this.application.mutator.changeItem( note, (mutator) => { mutator.mutableContent.conflict_of = 'bar' @@ -329,10 +329,10 @@ describe('online conflict handling', function () { it('setting property on two clients simultaneously should create conflict', async function () { const note = await Factory.createMappedNote(this.application) - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) this.expectedItemCount++ - await this.application.mutator.changeAndSaveItem( + await this.application.changeAndSaveItem( note, (mutator) => { // client A @@ -369,12 +369,12 @@ describe('online conflict handling', function () { const note = await Factory.createMappedNote(this.application) const originalPayload = note.payloadRepresentation() this.expectedItemCount++ - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) await this.application.syncService.sync(syncOptions) expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount) // client A - await this.application.itemManager.setItemToBeDeleted(note) + await this.application.mutator.setItemToBeDeleted(note) await this.application.syncService.sync(syncOptions) this.expectedItemCount-- expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount) @@ -387,10 +387,10 @@ describe('online conflict handling', function () { deleted: false, updated_at: Factory.yesterday(), }) - await this.application.itemManager.emitItemsFromPayloads([mutatedPayload], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([mutatedPayload], PayloadEmitSource.LocalChanged) const resultNote = this.application.itemManager.findItem(note.uuid) expect(resultNote.uuid).to.equal(note.uuid) - await this.application.itemManager.setItemDirty(resultNote) + await this.application.mutator.setItemDirty(resultNote) await this.application.syncService.sync(syncOptions) // We expect that this item is now gone for good, and a duplicate has not been created. @@ -400,7 +400,7 @@ describe('online conflict handling', function () { it('if server says not deleted but client says deleted, keep server state', async function () { const note = await Factory.createMappedNote(this.application) - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) this.expectedItemCount++ // client A @@ -426,7 +426,7 @@ describe('online conflict handling', function () { it('should create conflict if syncing an item that is stale', async function () { let note = await Factory.createMappedNote(this.application) - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) await this.application.syncService.sync(syncOptions) note = this.application.items.findItem(note.uuid) expect(note.dirty).to.equal(false) @@ -462,7 +462,7 @@ describe('online conflict handling', function () { it('creating conflict with exactly equal content should keep us in sync', async function () { const note = await Factory.createMappedNote(this.application) - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) this.expectedItemCount++ await this.application.syncService.sync(syncOptions) @@ -505,7 +505,7 @@ describe('online conflict handling', function () { for (const note of this.application.itemManager.getDisplayableNotes()) { /** First modify the item without saving so that * our local contents digress from the server's */ - await this.application.itemManager.changeItem(note, (mutator) => { + await this.application.mutator.changeItem(note, (mutator) => { mutator.text = '1' }) @@ -530,18 +530,18 @@ describe('online conflict handling', function () { const payload1 = Factory.createStorageItemPayload(ContentType.Tag) const payload2 = Factory.createStorageItemPayload(ContentType.UserPrefs) this.expectedItemCount -= 1 /** auto-created user preferences */ - await this.application.itemManager.emitItemsFromPayloads([payload1, payload2], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([payload1, payload2], PayloadEmitSource.LocalChanged) this.expectedItemCount += 2 let tag = this.application.itemManager.getItems(ContentType.Tag)[0] let userPrefs = this.application.itemManager.getItems(ContentType.UserPrefs)[0] expect(tag).to.be.ok expect(userPrefs).to.be.ok - tag = await this.application.itemManager.changeItem(tag, (mutator) => { + tag = await this.application.mutator.changeItem(tag, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(userPrefs) }) - await this.application.itemManager.setItemDirty(userPrefs) + await this.application.mutator.setItemDirty(userPrefs) userPrefs = this.application.items.findItem(userPrefs.uuid) expect(this.application.itemManager.itemsReferencingItem(userPrefs).length).to.equal(1) @@ -599,7 +599,7 @@ describe('online conflict handling', function () { */ let tag = await Factory.createMappedTag(this.application) let note = await Factory.createMappedNote(this.application) - tag = await this.application.mutator.changeAndSaveItem( + tag = await this.application.changeAndSaveItem( tag, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(note) @@ -608,7 +608,7 @@ describe('online conflict handling', function () { undefined, syncOptions, ) - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) this.expectedItemCount += 2 await this.application.syncService.sync(syncOptions) @@ -663,18 +663,18 @@ describe('online conflict handling', function () { const baseTitle = 'base title' /** Change the note */ - const noteAfterChange = await this.application.itemManager.changeItem(note, (mutator) => { + const noteAfterChange = await this.application.mutator.changeItem(note, (mutator) => { mutator.title = baseTitle }) await this.application.sync.sync() /** Simulate a dropped response by reverting the note back its post-change, pre-sync state */ - const retroNote = await this.application.itemManager.emitItemFromPayload(noteAfterChange.payload) + const retroNote = await this.application.mutator.emitItemFromPayload(noteAfterChange.payload) expect(retroNote.serverUpdatedAt.getTime()).to.equal(noteAfterChange.serverUpdatedAt.getTime()) /** Change the item to its final title and sync */ const finalTitle = 'final title' - await this.application.itemManager.changeItem(note, (mutator) => { + await this.application.mutator.changeItem(note, (mutator) => { mutator.title = finalTitle }) await this.application.sync.sync() @@ -708,7 +708,7 @@ describe('online conflict handling', function () { errorDecrypting: true, dirty: true, }) - await this.application.itemManager.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged) /** * Retrieve this note from the server by clearing sync token @@ -758,7 +758,7 @@ describe('online conflict handling', function () { email: Utils.generateUuid(), password: Utils.generateUuid(), }) - await newApp.itemManager.emitItemsFromPayloads(priorData.map((i) => i.payload)) + await newApp.mutator.emitItemsFromPayloads(priorData.map((i) => i.payload)) await newApp.syncService.markAllItemsAsNeedingSyncAndPersist() await newApp.syncService.sync(syncOptions) expect(newApp.payloadManager.invalidPayloads.length).to.equal(0) @@ -786,7 +786,7 @@ describe('online conflict handling', function () { password: password, }) Factory.handlePasswordChallenges(newApp, password) - await newApp.mutator.importData(backupFile, true) + await newApp.importData(backupFile, true) expect(newApp.itemManager.getDisplayableTags().length).to.equal(1) expect(newApp.itemManager.getDisplayableNotes().length).to.equal(1) await Factory.safeDeinit(newApp) @@ -801,7 +801,7 @@ describe('online conflict handling', function () { await createSyncedNoteWithTag(this.application) const tag = this.application.itemManager.getDisplayableTags()[0] const note2 = await Factory.createMappedNote(this.application) - await this.application.mutator.changeAndSaveItem(tag, (mutator) => { + await this.application.changeAndSaveItem(tag, (mutator) => { mutator.e2ePendingRefactor_addItemAsRelationship(note2) }) let backupFile = await this.application.createEncryptedBackupFileForAutomatedDesktopBackups() @@ -821,7 +821,7 @@ describe('online conflict handling', function () { password: password, }) Factory.handlePasswordChallenges(newApp, password) - await newApp.mutator.importData(backupFile, true) + await newApp.importData(backupFile, true) const newTag = newApp.itemManager.getDisplayableTags()[0] const notes = newApp.items.referencesForItem(newTag) expect(notes.length).to.equal(2) @@ -855,7 +855,7 @@ describe('online conflict handling', function () { }, dirty: true, }) - await this.application.itemManager.emitItemFromPayload(modified) + await this.application.mutator.emitItemFromPayload(modified) await this.application.sync.sync() expect(this.application.itemManager.getDisplayableNotes().length).to.equal(1) await this.sharedFinalAssertions() @@ -879,7 +879,7 @@ describe('online conflict handling', function () { dirty: true, }) this.expectedItemCount++ - await this.application.itemManager.emitItemFromPayload(modified) + await this.application.mutator.emitItemFromPayload(modified) await this.application.sync.sync() expect(this.application.itemManager.getDisplayableNotes().length).to.equal(2) await this.sharedFinalAssertions() @@ -911,7 +911,7 @@ describe('online conflict handling', function () { dirty: true, }) this.expectedItemCount++ - await this.application.itemManager.emitItemFromPayload(modified) + await this.application.mutator.emitItemFromPayload(modified) await this.application.sync.sync() expect(this.application.itemManager.getDisplayableNotes().length).to.equal(2) await this.sharedFinalAssertions() diff --git a/packages/snjs/mocha/sync_tests/integrity.test.js b/packages/snjs/mocha/sync_tests/integrity.test.js index d91339f96..a81be23d2 100644 --- a/packages/snjs/mocha/sync_tests/integrity.test.js +++ b/packages/snjs/mocha/sync_tests/integrity.test.js @@ -1,6 +1,6 @@ /* eslint-disable no-unused-expressions */ /* eslint-disable no-undef */ -import { BaseItemCounts } from '../lib/Applications.js' +import { BaseItemCounts } from '../lib/BaseItemCounts.js' import * as Factory from '../lib/factory.js' chai.use(chaiAsPromised) const expect = chai.expect @@ -15,7 +15,7 @@ describe('sync integrity', () => { }) beforeEach(async function () { - this.expectedItemCount = BaseItemCounts.DefaultItems + this.expectedItemCount = BaseItemCounts.DefaultItemsWithAccount this.application = await Factory.createInitAppWithFakeCrypto() this.email = UuidGenerator.GenerateUuid() this.password = UuidGenerator.GenerateUuid() @@ -44,7 +44,7 @@ describe('sync integrity', () => { }) it('should detect when out of sync', async function () { - const item = await this.application.itemManager.emitItemFromPayload( + const item = await this.application.mutator.emitItemFromPayload( Factory.createNotePayload(), PayloadEmitSource.LocalChanged, ) @@ -60,7 +60,7 @@ describe('sync integrity', () => { }) it('should self heal after out of sync', async function () { - const item = await this.application.itemManager.emitItemFromPayload( + const item = await this.application.mutator.emitItemFromPayload( Factory.createNotePayload(), PayloadEmitSource.LocalChanged, ) diff --git a/packages/snjs/mocha/sync_tests/notes_tags.test.js b/packages/snjs/mocha/sync_tests/notes_tags.test.js index f1219a749..0dd7d584a 100644 --- a/packages/snjs/mocha/sync_tests/notes_tags.test.js +++ b/packages/snjs/mocha/sync_tests/notes_tags.test.js @@ -33,7 +33,7 @@ describe('notes + tags syncing', function () { it('syncing an item then downloading it should include items_key_id', async function () { const note = await Factory.createMappedNote(this.application) - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) await this.application.syncService.sync(syncOptions) await this.application.payloadManager.resetState() await this.application.itemManager.resetState() @@ -52,14 +52,14 @@ describe('notes + tags syncing', function () { const notePayload = pair[0] const tagPayload = pair[1] - await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged) const note = this.application.itemManager.getItems([ContentType.Note])[0] const tag = this.application.itemManager.getItems([ContentType.Tag])[0] expect(this.application.itemManager.getDisplayableNotes().length).to.equal(1) expect(this.application.itemManager.getDisplayableTags().length).to.equal(1) for (let i = 0; i < 9; i++) { - await this.application.itemManager.setItemsDirty([note, tag]) + await this.application.mutator.setItemsDirty([note, tag]) await this.application.syncService.sync(syncOptions) this.application.syncService.clearSyncPositionTokens() expect(tag.content.references.length).to.equal(1) @@ -76,10 +76,10 @@ describe('notes + tags syncing', function () { const pair = createRelatedNoteTagPairPayload() const notePayload = pair[0] const tagPayload = pair[1] - await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged) const originalNote = this.application.itemManager.getDisplayableNotes()[0] const originalTag = this.application.itemManager.getDisplayableTags()[0] - await this.application.itemManager.setItemsDirty([originalNote, originalTag]) + await this.application.mutator.setItemsDirty([originalNote, originalTag]) await this.application.syncService.sync(syncOptions) @@ -109,12 +109,12 @@ describe('notes + tags syncing', function () { const pair = createRelatedNoteTagPairPayload() const notePayload = pair[0] const tagPayload = pair[1] - await this.application.itemManager.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads([notePayload, tagPayload], PayloadEmitSource.LocalChanged) let note = this.application.itemManager.getDisplayableNotes()[0] let tag = this.application.itemManager.getDisplayableTags()[0] expect(this.application.itemManager.itemsReferencingItem(note).length).to.equal(1) - await this.application.itemManager.setItemsDirty([note, tag]) + await this.application.mutator.setItemsDirty([note, tag]) await this.application.syncService.sync(syncOptions) await this.application.syncService.clearSyncPositionTokens() diff --git a/packages/snjs/mocha/sync_tests/offline.test.js b/packages/snjs/mocha/sync_tests/offline.test.js index 25cf65028..c5ca32cea 100644 --- a/packages/snjs/mocha/sync_tests/offline.test.js +++ b/packages/snjs/mocha/sync_tests/offline.test.js @@ -1,6 +1,6 @@ /* eslint-disable no-unused-expressions */ /* eslint-disable no-undef */ -import { BaseItemCounts } from '../lib/Applications.js' +import { BaseItemCounts } from '../lib/BaseItemCounts.js' import * as Factory from '../lib/factory.js' chai.use(chaiAsPromised) const expect = chai.expect @@ -31,6 +31,21 @@ describe('offline syncing', () => { localStorage.clear() }) + it('uuid alternation should delete original payload', async function () { + const note = await Factory.createMappedNote(this.application) + this.expectedItemCount++ + + await Factory.alternateUuidForItem(this.application, note.uuid) + await this.application.sync.sync(syncOptions) + + const notes = this.application.itemManager.getDisplayableNotes() + expect(notes.length).to.equal(1) + expect(notes[0].uuid).to.not.equal(note.uuid) + + const items = this.application.itemManager.allTrackedItems() + expect(items.length).to.equal(this.expectedItemCount) + }) + it('should sync item with no passcode', async function () { let note = await Factory.createMappedNote(this.application) expect(Uuids(this.application.itemManager.getDirtyItems()).includes(note.uuid)) diff --git a/packages/snjs/mocha/sync_tests/online.test.js b/packages/snjs/mocha/sync_tests/online.test.js index 1ab803218..2d902f5cf 100644 --- a/packages/snjs/mocha/sync_tests/online.test.js +++ b/packages/snjs/mocha/sync_tests/online.test.js @@ -1,5 +1,5 @@ /* eslint-disable no-undef */ -import { BaseItemCounts } from '../lib/Applications.js' +import { BaseItemCounts } from '../lib/BaseItemCounts.js' import * as Factory from '../lib/factory.js' import * as Utils from '../lib/Utils.js' chai.use(chaiAsPromised) @@ -15,7 +15,7 @@ describe('online syncing', function () { beforeEach(async function () { localStorage.clear() - this.expectedItemCount = BaseItemCounts.DefaultItems + this.expectedItemCount = BaseItemCounts.DefaultItemsWithAccount this.context = await Factory.createAppContext() await this.context.launch() @@ -43,8 +43,10 @@ describe('online syncing', function () { afterEach(async function () { expect(this.application.syncService.isOutOfSync()).to.equal(false) + const items = this.application.itemManager.allTrackedItems() expect(items.length).to.equal(this.expectedItemCount) + const rawPayloads = await this.application.diskStorageService.getAllRawPayloads() expect(rawPayloads.length).to.equal(this.expectedItemCount) await Factory.safeDeinit(this.application) @@ -119,18 +121,6 @@ describe('online syncing', function () { await Factory.sleep(0.5) }).timeout(20000) - it('uuid alternation should delete original payload', async function () { - this.application = await Factory.signOutApplicationAndReturnNew(this.application) - const note = await Factory.createMappedNote(this.application) - this.expectedItemCount++ - await Factory.alternateUuidForItem(this.application, note.uuid) - await this.application.sync.sync(syncOptions) - - const notes = this.application.itemManager.getDisplayableNotes() - expect(notes.length).to.equal(1) - expect(notes[0].uuid).to.not.equal(note.uuid) - }) - it('having offline data then signing in should not alternate uuid and merge with account', async function () { this.application = await Factory.signOutApplicationAndReturnNew(this.application) const note = await Factory.createMappedNote(this.application) @@ -222,7 +212,7 @@ describe('online syncing', function () { this.application = await Factory.signOutApplicationAndReturnNew(this.application) const promise = new Promise((resolve) => { this.application.syncService.addEventObserver(async (event) => { - if (event === SyncEvent.SingleRoundTripSyncCompleted) { + if (event === SyncEvent.PaginatedSyncRequestCompleted) { const note = this.application.items.findItem(originalNote.uuid) if (note) { expect(note.dirty).to.not.be.ok @@ -241,7 +231,7 @@ describe('online syncing', function () { expect(this.application.itemManager.getDisplayableItemsKeys().length).to.equal(1) const note = await Factory.createMappedNote(this.application) this.expectedItemCount++ - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) await this.application.syncService.sync(syncOptions) const rawPayloads = await this.application.diskStorageService.getAllRawPayloads() const notePayload = noteObjectsFromObjects(rawPayloads) @@ -283,7 +273,7 @@ describe('online syncing', function () { const originalTitle = note.content.title - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) await this.application.syncService.sync(syncOptions) const encrypted = CreateEncryptedServerSyncPushPayload( @@ -299,7 +289,7 @@ describe('online syncing', function () { errorDecrypting: true, }) - const items = await this.application.itemManager.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged) + const items = await this.application.mutator.emitItemsFromPayloads([errorred], PayloadEmitSource.LocalChanged) const mappedItem = this.application.itemManager.findAnyItem(errorred.uuid) @@ -311,7 +301,7 @@ describe('online syncing', function () { }, }) - const mappedItems2 = await this.application.itemManager.emitItemsFromPayloads( + const mappedItems2 = await this.application.mutator.emitItemsFromPayloads( [decryptedPayload], PayloadEmitSource.LocalChanged, ) @@ -336,14 +326,14 @@ describe('online syncing', function () { let note = await Factory.createMappedNote(this.application) this.expectedItemCount++ - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) await this.application.syncService.sync(syncOptions) note = this.application.items.findItem(note.uuid) expect(note.dirty).to.equal(false) expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount) - await this.application.itemManager.setItemToBeDeleted(note) + await this.application.mutator.setItemToBeDeleted(note) note = this.application.items.findAnyItem(note.uuid) expect(note.dirty).to.equal(true) this.expectedItemCount-- @@ -361,7 +351,7 @@ describe('online syncing', function () { it('retrieving item with no content should correctly map local state', async function () { const note = await Factory.createMappedNote(this.application) - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) await this.application.syncService.sync(syncOptions) const syncToken = await this.application.syncService.getLastSyncToken() @@ -370,7 +360,7 @@ describe('online syncing', function () { expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount) // client A - await this.application.itemManager.setItemToBeDeleted(note) + await this.application.mutator.setItemToBeDeleted(note) await this.application.syncService.sync(syncOptions) // Subtract 1 @@ -399,7 +389,7 @@ describe('online syncing', function () { await Factory.sleep(0.1) - await this.application.itemManager.changeItem(note, (mutator) => { + await this.application.mutator.changeItem(note, (mutator) => { mutator.title = 'latest title' }) @@ -427,7 +417,7 @@ describe('online syncing', function () { await Factory.sleep(0.1) - await this.application.itemManager.setItemToBeDeleted(note) + await this.application.mutator.setItemToBeDeleted(note) this.expectedItemCount-- @@ -444,8 +434,8 @@ describe('online syncing', function () { it('items that are never synced and deleted should not be uploaded to server', async function () { const note = await Factory.createMappedNote(this.application) - await this.application.itemManager.setItemDirty(note) - await this.application.itemManager.setItemToBeDeleted(note) + await this.application.mutator.setItemDirty(note) + await this.application.mutator.setItemToBeDeleted(note) let success = true let didCompleteRelevantSync = false @@ -457,7 +447,7 @@ describe('online syncing', function () { if (!beginCheckingResponse) { return } - if (!didCompleteRelevantSync && eventName === SyncEvent.SingleRoundTripSyncCompleted) { + if (!didCompleteRelevantSync && eventName === SyncEvent.PaginatedSyncRequestCompleted) { didCompleteRelevantSync = true const response = data const matching = response.savedPayloads.find((p) => p.uuid === note.uuid) @@ -474,20 +464,20 @@ describe('online syncing', function () { it('items that are deleted after download first sync complete should not be uploaded to server', async function () { /** The singleton manager may delete items are download first. We dont want those uploaded to server. */ const note = await Factory.createMappedNote(this.application) - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) let success = true let didCompleteRelevantSync = false let beginCheckingResponse = false this.application.syncService.addEventObserver(async (eventName, data) => { if (eventName === SyncEvent.DownloadFirstSyncCompleted) { - await this.application.itemManager.setItemToBeDeleted(note) + await this.application.mutator.setItemToBeDeleted(note) beginCheckingResponse = true } if (!beginCheckingResponse) { return } - if (!didCompleteRelevantSync && eventName === SyncEvent.SingleRoundTripSyncCompleted) { + if (!didCompleteRelevantSync && eventName === SyncEvent.PaginatedSyncRequestCompleted) { didCompleteRelevantSync = true const response = data const matching = response.savedPayloads.find((p) => p.uuid === note.uuid) @@ -527,7 +517,7 @@ describe('online syncing', function () { const decryptionResults = await this.application.protocolService.decryptSplit(keyedSplit) - await this.application.itemManager.emitItemsFromPayloads(decryptionResults, PayloadEmitSource.LocalChanged) + await this.application.mutator.emitItemsFromPayloads(decryptionResults, PayloadEmitSource.LocalChanged) expect(this.application.itemManager.allTrackedItems().length).to.equal(this.expectedItemCount) @@ -543,7 +533,7 @@ describe('online syncing', function () { const largeItemCount = SyncUpDownLimit + 10 for (let i = 0; i < largeItemCount; i++) { const note = await Factory.createMappedNote(this.application) - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) } this.expectedItemCount += largeItemCount @@ -558,7 +548,7 @@ describe('online syncing', function () { const largeItemCount = SyncUpDownLimit + 10 for (let i = 0; i < largeItemCount; i++) { const note = await Factory.createMappedNote(this.application) - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) } /** Upload */ this.application.syncService.sync({ awaitAll: true, checkIntegrity: false }) @@ -583,7 +573,7 @@ describe('online syncing', function () { it('syncing an item should storage it encrypted', async function () { const note = await Factory.createMappedNote(this.application) - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) await this.application.syncService.sync(syncOptions) this.expectedItemCount++ const rawPayloads = await this.application.diskStorageService.getAllRawPayloads() @@ -593,7 +583,7 @@ describe('online syncing', function () { it('syncing an item before data load should storage it encrypted', async function () { const note = await Factory.createMappedNote(this.application) - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) this.expectedItemCount++ /** Simulate database not loaded */ @@ -610,7 +600,7 @@ describe('online syncing', function () { it('saving an item after sync should persist it with content property', async function () { const note = await Factory.createMappedNote(this.application) const text = Factory.randomString(10000) - await this.application.mutator.changeAndSaveItem( + await this.application.changeAndSaveItem( note, (mutator) => { mutator.text = text @@ -634,7 +624,7 @@ describe('online syncing', function () { expect(this.application.itemManager.getDirtyItems().length).to.equal(0) let note = await Factory.createMappedNote(this.application) - note = await this.application.itemManager.changeItem(note, (mutator) => { + note = await this.application.mutator.changeItem(note, (mutator) => { mutator.text = `${Math.random()}` }) /** This sync request should exit prematurely as we called ut_setDatabaseNotLoaded */ @@ -705,13 +695,13 @@ describe('online syncing', function () { it('valid sync date tracking', async function () { let note = await Factory.createMappedNote(this.application) - note = await this.application.itemManager.setItemDirty(note) + note = await this.application.mutator.setItemDirty(note) this.expectedItemCount++ expect(note.dirty).to.equal(true) expect(note.payload.dirtyIndex).to.be.at.most(getCurrentDirtyIndex()) - note = await this.application.itemManager.changeItem(note, (mutator) => { + note = await this.application.mutator.changeItem(note, (mutator) => { mutator.text = `${Math.random()}` }) const sync = this.application.sync.sync(syncOptions) @@ -748,7 +738,7 @@ describe('online syncing', function () { * It will do based on comparing whether item.dirtyIndex > item.globalDirtyIndexAtLastSync */ let note = await Factory.createMappedNote(this.application) - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) this.expectedItemCount++ // client A. Don't await, we want to do other stuff. @@ -759,12 +749,12 @@ describe('online syncing', function () { // While that sync is going on, we want to modify this item many times. const text = `${Math.random()}` - note = await this.application.itemManager.changeItem(note, (mutator) => { + note = await this.application.mutator.changeItem(note, (mutator) => { mutator.text = text }) - await this.application.itemManager.setItemDirty(note) - await this.application.itemManager.setItemDirty(note) - await this.application.itemManager.setItemDirty(note) + await this.application.mutator.setItemDirty(note) + await this.application.mutator.setItemDirty(note) + await this.application.mutator.setItemDirty(note) expect(note.payload.dirtyIndex).to.be.above(note.payload.globalDirtyIndexAtLastSync) // Now do a regular sync with no latency. @@ -817,7 +807,7 @@ describe('online syncing', function () { setTimeout( async function () { - await this.application.itemManager.changeItem(note, (mutator) => { + await this.application.mutator.changeItem(note, (mutator) => { mutator.text = newText }) }.bind(this), @@ -862,9 +852,9 @@ describe('online syncing', function () { const newText = `${Math.random()}` this.application.syncService.addEventObserver(async (eventName) => { - if (eventName === SyncEvent.SyncWillBegin && !didPerformMutatation) { + if (eventName === SyncEvent.SyncDidBeginProcessing && !didPerformMutatation) { didPerformMutatation = true - await this.application.itemManager.changeItem(note, (mutator) => { + await this.application.mutator.changeItem(note, (mutator) => { mutator.text = newText }) } @@ -898,7 +888,7 @@ describe('online syncing', function () { dirtyIndex: changed[0].payload.globalDirtyIndexAtLastSync + 1, }) - await this.application.itemManager.emitItemFromPayload(mutated) + await this.application.mutator.emitItemFromPayload(mutated) } }) @@ -916,6 +906,7 @@ describe('online syncing', function () { const note = await Factory.createSyncedNote(this.application) const preDeleteSyncToken = await this.application.syncService.getLastSyncToken() await this.application.mutator.deleteItem(note) + await this.application.sync.sync() await this.application.syncService.setLastSyncToken(preDeleteSyncToken) await this.application.sync.sync(syncOptions) expect(this.application.itemManager.items.length).to.equal(this.expectedItemCount) @@ -938,7 +929,7 @@ describe('online syncing', function () { dirty: true, }) - await this.application.itemManager.emitItemFromPayload(errored) + await this.application.payloadManager.emitPayload(errored) await this.application.sync.sync(syncOptions) const updatedNote = this.application.items.findAnyItem(note.uuid) @@ -966,7 +957,7 @@ describe('online syncing', function () { }, }) - await this.application.syncService.handleSuccessServerResponse({ payloadsSavedOrSaving: [] }, response) + await this.application.syncService.handleSuccessServerResponse({ payloadsSavedOrSaving: [], options: {} }, response) expect(this.application.payloadManager.findOne(invalidPayload.uuid)).to.not.be.ok expect(this.application.payloadManager.findOne(validPayload.uuid)).to.be.ok @@ -995,7 +986,7 @@ describe('online syncing', function () { content: {}, }) this.expectedItemCount++ - await this.application.itemManager.emitItemsFromPayloads([payload]) + await this.application.mutator.emitItemsFromPayloads([payload]) await this.application.sync.sync(syncOptions) /** Item should no longer be dirty, otherwise it would keep syncing */ @@ -1006,7 +997,7 @@ describe('online syncing', function () { it('should call onPresyncSave before sync begins', async function () { const events = [] this.application.syncService.addEventObserver((event) => { - if (event === SyncEvent.SyncWillBegin) { + if (event === SyncEvent.SyncDidBeginProcessing) { events.push('sync-will-begin') } }) @@ -1032,6 +1023,7 @@ describe('online syncing', function () { const note = await Factory.createSyncedNote(this.application) await this.application.mutator.deleteItem(note) + await this.application.sync.sync() expect(conditionMet).to.equal(true) }) diff --git a/packages/snjs/mocha/test.html b/packages/snjs/mocha/test.html index b67b53353..e722ea517 100644 --- a/packages/snjs/mocha/test.html +++ b/packages/snjs/mocha/test.html @@ -12,14 +12,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -103,4 +81,4 @@

- + \ No newline at end of file diff --git a/packages/snjs/mocha/upgrading.test.js b/packages/snjs/mocha/upgrading.test.js index cbe08696b..b7c6fc059 100644 --- a/packages/snjs/mocha/upgrading.test.js +++ b/packages/snjs/mocha/upgrading.test.js @@ -173,7 +173,7 @@ describe('upgrading', () => { it('protocol version should be upgraded on password change', async function () { /** Delete default items key that is created on launch */ const itemsKey = await this.application.protocolService.getSureDefaultItemsKey() - await this.application.itemManager.setItemToBeDeleted(itemsKey) + await this.application.mutator.setItemToBeDeleted(itemsKey) expect(Uuids(this.application.itemManager.getDisplayableItemsKeys()).includes(itemsKey.uuid)).to.equal(false) Factory.createMappedNote(this.application) diff --git a/packages/snjs/mocha/vaults/asymmetric-messages.test.js b/packages/snjs/mocha/vaults/asymmetric-messages.test.js new file mode 100644 index 000000000..449a675a2 --- /dev/null +++ b/packages/snjs/mocha/vaults/asymmetric-messages.test.js @@ -0,0 +1,277 @@ +import * as Factory from '../lib/factory.js' +import * as Collaboration from '../lib/Collaboration.js' + +chai.use(chaiAsPromised) +const expect = chai.expect + +describe('asymmetric messages', function () { + this.timeout(Factory.TwentySecondTimeout) + + let context + let service + + afterEach(async function () { + await context.deinit() + localStorage.clear() + }) + + beforeEach(async function () { + localStorage.clear() + + context = await Factory.createAppContextWithRealCrypto() + + await context.launch() + await context.register() + + service = context.asymmetric + }) + + it('should not trust message if the trusted payload data recipientUuid does not match the message user uuid', async () => { + console.error('TODO: implement') + }) + + it('should delete message after processing it', async () => { + const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context) + + const eventData = { + oldKeyPair: context.encryption.getKeyPair(), + oldSigningKeyPair: context.encryption.getSigningKeyPair(), + newKeyPair: context.encryption.getKeyPair(), + newSigningKeyPair: context.encryption.getSigningKeyPair(), + } + + await service.sendOwnContactChangeEventToAllContacts(eventData) + + const deleteFunction = sinon.spy(contactContext.asymmetric, 'deleteMessageAfterProcessing') + + const promise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() + + await contactContext.sync() + + await promise + + expect(deleteFunction.callCount).to.equal(1) + + const messages = await contactContext.asymmetric.getInboundMessages() + expect(messages.length).to.equal(0) + + await deinitContactContext() + }) + + it('should send contact share message after trusted contact belonging to group changes', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault( + context, + sharedVault, + ) + + await Collaboration.acceptAllInvites(thirdPartyContext) + + const sendContactSharePromise = context.resolveWhenSharedVaultServiceSendsContactShareMessage() + + await context.contacts.createOrEditTrustedContact({ + contactUuid: thirdPartyContext.userUuid, + publicKey: thirdPartyContext.publicKey, + signingPublicKey: thirdPartyContext.signingPublicKey, + name: 'Changed 3rd Party Name', + }) + + await sendContactSharePromise + + const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() + + await contactContext.sync() + await completedProcessingMessagesPromise + + const updatedContact = contactContext.contacts.findTrustedContact(thirdPartyContext.userUuid) + expect(updatedContact.name).to.equal('Changed 3rd Party Name') + + await deinitContactContext() + await deinitThirdPartyContext() + }) + + it('should not send contact share message to self or to contact who is changed', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault( + context, + sharedVault, + ) + const handleInitialContactShareMessage = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() + + await Collaboration.acceptAllInvites(thirdPartyContext) + + await contactContext.sync() + await handleInitialContactShareMessage + + const sendContactSharePromise = context.resolveWhenSharedVaultServiceSendsContactShareMessage() + + await context.contacts.createOrEditTrustedContact({ + contactUuid: thirdPartyContext.userUuid, + publicKey: thirdPartyContext.publicKey, + signingPublicKey: thirdPartyContext.signingPublicKey, + name: 'Changed 3rd Party Name', + }) + + await sendContactSharePromise + + const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedContactShareMessage') + const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedContactShareMessage') + const thirdPartySpy = sinon.spy(thirdPartyContext.asymmetric, 'handleTrustedContactShareMessage') + + await context.sync() + await contactContext.sync() + await thirdPartyContext.sync() + + expect(firstPartySpy.callCount).to.equal(0) + expect(secondPartySpy.callCount).to.equal(1) + expect(thirdPartySpy.callCount).to.equal(0) + + await deinitThirdPartyContext() + await deinitContactContext() + }) + + it('should send shared vault root key change message after root key change', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + await context.vaults.rotateVaultRootKey(sharedVault) + + const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage') + const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage') + + await context.sync() + await contactContext.sync() + + expect(firstPartySpy.callCount).to.equal(0) + expect(secondPartySpy.callCount).to.equal(1) + + await deinitContactContext() + }) + + it('should send shared vault metadata change message after shared vault name change', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + await context.vaults.changeVaultNameAndDescription(sharedVault, { + name: 'New Name', + description: 'New Description', + }) + + const firstPartySpy = sinon.spy(context.asymmetric, 'handleVaultMetadataChangedMessage') + const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleVaultMetadataChangedMessage') + + await context.sync() + await contactContext.sync() + + expect(firstPartySpy.callCount).to.equal(0) + expect(secondPartySpy.callCount).to.equal(1) + + const updatedVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) + expect(updatedVault.name).to.equal('New Name') + expect(updatedVault.description).to.equal('New Description') + + await deinitContactContext() + }) + + it('should send sender keypair changed message to trusted contacts', async () => { + const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context) + + await context.changePassword('new password') + + const firstPartySpy = sinon.spy(context.asymmetric, 'handleTrustedSenderKeypairChangedMessage') + const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSenderKeypairChangedMessage') + + await context.sync() + + const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() + await contactContext.sync() + await completedProcessingMessagesPromise + + expect(firstPartySpy.callCount).to.equal(0) + expect(secondPartySpy.callCount).to.equal(1) + + const contact = contactContext.contacts.findTrustedContact(context.userUuid) + expect(contact.publicKeySet.encryption).to.equal(context.publicKey) + expect(contact.publicKeySet.signing).to.equal(context.signingPublicKey) + + await deinitContactContext() + }) + + it('should process sender keypair changed message', async () => { + const { contactContext, deinitContactContext } = await Collaboration.createContactContext() + await Collaboration.createTrustedContactForUserOfContext(context, contactContext) + await Collaboration.createTrustedContactForUserOfContext(contactContext, context) + const originalContact = contactContext.contacts.findTrustedContact(context.userUuid) + + await context.changePassword('new_password') + + const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() + await contactContext.sync() + await completedProcessingMessagesPromise + + const updatedContact = contactContext.contacts.findTrustedContact(context.userUuid) + + expect(updatedContact.publicKeySet.encryption).to.not.equal(originalContact.publicKeySet.encryption) + expect(updatedContact.publicKeySet.signing).to.not.equal(originalContact.publicKeySet.signing) + + expect(updatedContact.publicKeySet.encryption).to.equal(context.publicKey) + expect(updatedContact.publicKeySet.signing).to.equal(context.signingPublicKey) + + await deinitContactContext() + }) + + it('sender keypair changed message should be signed using old key pair', async () => { + const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context) + + const oldKeyPair = context.encryption.getKeyPair() + const oldSigningKeyPair = context.encryption.getSigningKeyPair() + + await context.changePassword('new password') + + const secondPartySpy = sinon.spy(contactContext.asymmetric, 'handleTrustedSenderKeypairChangedMessage') + + await context.sync() + const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() + await contactContext.sync() + await completedProcessingMessagesPromise + + const message = secondPartySpy.args[0][0] + const encryptedMessage = message.encrypted_message + + const publicKeySet = + contactContext.encryption.getSenderPublicKeySetFromAsymmetricallyEncryptedString(encryptedMessage) + + expect(publicKeySet.encryption).to.equal(oldKeyPair.publicKey) + expect(publicKeySet.signing).to.equal(oldSigningKeyPair.publicKey) + + await deinitContactContext() + }) + + it('sender keypair changed message should contain new keypair and be trusted', async () => { + const { contactContext, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context) + + await context.changePassword('new password') + + const newKeyPair = context.encryption.getKeyPair() + const newSigningKeyPair = context.encryption.getSigningKeyPair() + + const completedProcessingMessagesPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() + await contactContext.sync() + await completedProcessingMessagesPromise + + const updatedContact = contactContext.contacts.findTrustedContact(context.userUuid) + expect(updatedContact.publicKeySet.encryption).to.equal(newKeyPair.publicKey) + expect(updatedContact.publicKeySet.signing).to.equal(newSigningKeyPair.publicKey) + + await deinitContactContext() + }) + + it('should delete all inbound messages after changing user password', async () => { + /** Messages to user are encrypted with old keypair and are no longer decryptable */ + console.error('TODO: implement test') + }) +}) diff --git a/packages/snjs/mocha/vaults/conflicts.test.js b/packages/snjs/mocha/vaults/conflicts.test.js new file mode 100644 index 000000000..5d52d3ca7 --- /dev/null +++ b/packages/snjs/mocha/vaults/conflicts.test.js @@ -0,0 +1,186 @@ +import * as Factory from '../lib/factory.js' +import * as Collaboration from '../lib/Collaboration.js' + +chai.use(chaiAsPromised) +const expect = chai.expect + +describe('shared vault conflicts', function () { + this.timeout(Factory.TwentySecondTimeout) + + let context + + afterEach(async function () { + await context.deinit() + localStorage.clear() + }) + + beforeEach(async function () { + localStorage.clear() + + context = await Factory.createAppContextWithRealCrypto() + + await context.launch() + await context.register() + }) + + it('after being removed from shared vault, attempting to sync previous vault item should result in SharedVaultNotMemberError. The item should be duplicated then removed.', async () => { + const { sharedVault, note, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) + + contactContext.lockSyncing() + await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid) + const promise = contactContext.resolveWithConflicts() + contactContext.unlockSyncing() + await contactContext.changeNoteTitleAndSync(note, 'new title') + const conflicts = await promise + await contactContext.sync() + + expect(conflicts.length).to.equal(1) + expect(conflicts[0].type).to.equal(ConflictType.SharedVaultNotMemberError) + expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note) + + const collaboratorNotes = contactContext.items.getDisplayableNotes() + expect(collaboratorNotes.length).to.equal(1) + expect(collaboratorNotes[0].duplicate_of).to.not.be.undefined + expect(collaboratorNotes[0].title).to.equal('new title') + + await deinitContactContext() + }) + + it('conflicts created should be associated with the vault', async () => { + const { note, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) + + await context.changeNoteTitle(note, 'new title first client') + await contactContext.changeNoteTitle(note, 'new title second client') + + const doneAddingConflictToSharedVault = contactContext.resolveWhenSavedSyncPayloadsIncludesItemThatIsDuplicatedOf( + note.uuid, + ) + + await context.sync({ desc: 'First client sync' }) + await contactContext.sync({ + desc: 'Second client sync with conflicts to be created', + }) + await doneAddingConflictToSharedVault + await context.sync({ desc: 'First client sync with conflicts to be pulled in' }) + + expect(context.items.invalidItems.length).to.equal(0) + expect(contactContext.items.invalidItems.length).to.equal(0) + + const collaboratorNotes = contactContext.items.getDisplayableNotes() + expect(collaboratorNotes.length).to.equal(2) + expect(collaboratorNotes.find((note) => !!note.duplicate_of)).to.not.be.undefined + + const originatorNotes = context.items.getDisplayableNotes() + expect(originatorNotes.length).to.equal(2) + expect(originatorNotes.find((note) => !!note.duplicate_of)).to.not.be.undefined + + await deinitContactContext() + }) + + it('attempting to modify note as read user should result in SharedVaultInsufficientPermissionsError', async () => { + const { note, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context, SharedVaultPermission.Read) + + const promise = contactContext.resolveWithConflicts() + await contactContext.changeNoteTitleAndSync(note, 'new title') + const conflicts = await promise + + expect(conflicts.length).to.equal(1) + expect(conflicts[0].type).to.equal(ConflictType.SharedVaultInsufficientPermissionsError) + expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note) + + await deinitContactContext() + }) + + it('should handle SharedVaultNotMemberError by duplicating item to user non-vault', async () => { + const { sharedVault, note, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) + + await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid) + await contactContext.changeNoteTitleAndSync(note, 'new title') + const notes = contactContext.notes + + expect(notes.length).to.equal(1) + expect(notes[0].title).to.equal('new title') + expect(notes[0].key_system_identifier).to.not.be.ok + expect(notes[0].duplicate_of).to.equal(note.uuid) + + await deinitContactContext() + }) + + it('attempting to save note to non-existent vault should result in SharedVaultNotMemberError conflict', async () => { + context.anticipateConsoleError( + 'Error decrypting contentKey from parameters', + 'An invalid shared vault uuid is being assigned to an item', + ) + const { note } = await Collaboration.createSharedVaultWithNote(context) + + const promise = context.resolveWithConflicts() + + const objectToSpy = context.application.sync + sinon.stub(objectToSpy, 'payloadsByPreparingForServer').callsFake(async (params) => { + objectToSpy.payloadsByPreparingForServer.restore() + const payloads = await objectToSpy.payloadsByPreparingForServer(params) + for (const payload of payloads) { + payload.shared_vault_uuid = 'non-existent-vault-uuid-123' + } + + return payloads + }) + + await context.changeNoteTitleAndSync(note, 'new-title') + const conflicts = await promise + + expect(conflicts.length).to.equal(1) + expect(conflicts[0].type).to.equal(ConflictType.SharedVaultNotMemberError) + expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note) + }) + + it('should create a non-vaulted copy if attempting to move item from vault to user and item belongs to someone else', async () => { + const { note, sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) + + const promise = contactContext.resolveWithConflicts() + await contactContext.vaults.removeItemFromVault(note) + const conflicts = await promise + + expect(conflicts.length).to.equal(1) + expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note) + + const duplicateNote = contactContext.findDuplicateNote(note.uuid) + expect(duplicateNote).to.not.be.undefined + expect(duplicateNote.key_system_identifier).to.not.be.ok + + const existingNote = contactContext.items.findItem(note.uuid) + expect(existingNote.key_system_identifier).to.equal(sharedVault.systemIdentifier) + + await deinitContactContext() + }) + + it('should created a non-vaulted copy if admin attempts to move item from vault to user if the item belongs to someone else', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + const note = await contactContext.createSyncedNote('foo', 'bar') + await Collaboration.moveItemToVault(contactContext, sharedVault, note) + await context.sync() + + const promise = context.resolveWithConflicts() + await context.vaults.removeItemFromVault(note) + const conflicts = await promise + + expect(conflicts.length).to.equal(1) + expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note) + + const duplicateNote = context.findDuplicateNote(note.uuid) + expect(duplicateNote).to.not.be.undefined + expect(duplicateNote.key_system_identifier).to.not.be.ok + + const existingNote = context.items.findItem(note.uuid) + expect(existingNote.key_system_identifier).to.equal(sharedVault.systemIdentifier) + + await deinitContactContext() + }) +}) diff --git a/packages/snjs/mocha/vaults/contacts.test.js b/packages/snjs/mocha/vaults/contacts.test.js new file mode 100644 index 000000000..ef158cba5 --- /dev/null +++ b/packages/snjs/mocha/vaults/contacts.test.js @@ -0,0 +1,83 @@ +import * as Factory from '../lib/factory.js' + +chai.use(chaiAsPromised) +const expect = chai.expect + +describe('contacts', function () { + this.timeout(Factory.TwentySecondTimeout) + + let context + + afterEach(async function () { + await context.deinit() + localStorage.clear() + }) + + beforeEach(async function () { + localStorage.clear() + + context = await Factory.createAppContextWithRealCrypto() + + await context.launch() + await context.register() + }) + + it('should create contact', async () => { + const contact = await context.contacts.createOrEditTrustedContact({ + name: 'John Doe', + publicKey: 'my_public_key', + signingPublicKey: 'my_signing_public_key', + contactUuid: '123', + }) + + expect(contact).to.not.be.undefined + expect(contact.name).to.equal('John Doe') + expect(contact.publicKeySet.encryption).to.equal('my_public_key') + expect(contact.publicKeySet.signing).to.equal('my_signing_public_key') + expect(contact.contactUuid).to.equal('123') + }) + + it('should create self contact on registration', async () => { + const selfContact = context.contacts.getSelfContact() + + expect(selfContact).to.not.be.undefined + + expect(selfContact.publicKeySet.encryption).to.equal(context.publicKey) + expect(selfContact.publicKeySet.signing).to.equal(context.signingPublicKey) + }) + + it('should create self contact on sign in if it does not exist', async () => { + let selfContact = context.contacts.getSelfContact() + await context.mutator.setItemToBeDeleted(selfContact) + await context.sync() + await context.signout() + + await context.signIn() + selfContact = context.contacts.getSelfContact() + expect(selfContact).to.not.be.undefined + }) + + it('should update self contact on password change', async () => { + const selfContact = context.contacts.getSelfContact() + + await context.changePassword('new_password') + + const updatedSelfContact = context.contacts.getSelfContact() + + expect(updatedSelfContact.publicKeySet.encryption).to.not.equal(selfContact.publicKeySet.encryption) + expect(updatedSelfContact.publicKeySet.signing).to.not.equal(selfContact.publicKeySet.signing) + + expect(updatedSelfContact.publicKeySet.encryption).to.equal(context.publicKey) + expect(updatedSelfContact.publicKeySet.signing).to.equal(context.signingPublicKey) + }) + + it('should not be able to delete self contact', async () => { + const selfContact = context.contacts.getSelfContact() + + await Factory.expectThrowsAsync(() => context.contacts.deleteContact(selfContact), 'Cannot delete self') + }) + + it('should not be able to delete a trusted contact if it belongs to a vault I administer', async () => { + console.error('TODO: implement test') + }) +}) diff --git a/packages/snjs/mocha/vaults/crypto.test.js b/packages/snjs/mocha/vaults/crypto.test.js new file mode 100644 index 000000000..c328e30a3 --- /dev/null +++ b/packages/snjs/mocha/vaults/crypto.test.js @@ -0,0 +1,204 @@ +import * as Factory from '../lib/factory.js' +import * as Collaboration from '../lib/Collaboration.js' + +chai.use(chaiAsPromised) +const expect = chai.expect + +describe('shared vault crypto', function () { + this.timeout(Factory.TwentySecondTimeout) + + let context + + afterEach(async function () { + await context.deinit() + localStorage.clear() + }) + + beforeEach(async function () { + localStorage.clear() + + context = await Factory.createAppContextWithRealCrypto() + + await context.launch() + await context.register() + }) + + describe('root key', () => { + it('root key loaded from disk should have keypairs', async () => { + const appIdentifier = context.identifier + await context.deinit() + + let recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) + await recreatedContext.launch() + + expect(recreatedContext.encryption.getKeyPair()).to.not.be.undefined + expect(recreatedContext.encryption.getSigningKeyPair()).to.not.be.undefined + }) + + it('changing user password should re-encrypt all key system root keys', async () => { + console.error('TODO: implement') + }) + + it('changing user password should re-encrypt all trusted contacts', async () => { + console.error('TODO: implement') + }) + }) + + describe('persistent content signature', () => { + it('storage payloads should include signatureData', async () => { + const { note, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) + + await contactContext.changeNoteTitleAndSync(note, 'new title') + await context.sync() + + const rawPayloads = await context.application.diskStorageService.getAllRawPayloads() + const noteRawPayload = rawPayloads.find((payload) => payload.uuid === note.uuid) + + expect(noteRawPayload.signatureData).to.not.be.undefined + + await deinitContactContext() + }) + + it('changing item content should erase existing signatureData', async () => { + const { note, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) + + await contactContext.changeNoteTitleAndSync(note, 'new title') + await context.sync() + + let updatedNote = context.items.findItem(note.uuid) + await context.changeNoteTitleAndSync(updatedNote, 'new title 2') + + updatedNote = context.items.findItem(note.uuid) + expect(updatedNote.signatureData).to.be.undefined + + await deinitContactContext() + }) + + it('encrypting an item into storage then loading it should verify authenticity of original content rather than most recent symmetric signature', async () => { + const { note, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) + + await contactContext.changeNoteTitleAndSync(note, 'new title') + + /** Override decrypt result to return failing signature */ + const objectToSpy = context.encryption + sinon.stub(objectToSpy, 'decryptSplit').callsFake(async (split) => { + objectToSpy.decryptSplit.restore() + + const decryptedPayloads = await objectToSpy.decryptSplit(split) + expect(decryptedPayloads.length).to.equal(1) + + const payload = decryptedPayloads[0] + const mutatedPayload = new DecryptedPayload({ + ...payload.ejected(), + signatureData: { + ...payload.signatureData, + result: { + ...payload.signatureData.result, + passes: false, + }, + }, + }) + + return [mutatedPayload] + }) + await context.sync() + + let updatedNote = context.items.findItem(note.uuid) + expect(updatedNote.content.title).to.equal('new title') + expect(updatedNote.signatureData.result.passes).to.equal(false) + + const appIdentifier = context.identifier + await context.deinit() + + let recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) + await recreatedContext.launch() + + updatedNote = recreatedContext.items.findItem(note.uuid) + expect(updatedNote.signatureData.result.passes).to.equal(false) + + /** Changing the content now should clear failing signature */ + await recreatedContext.changeNoteTitleAndSync(updatedNote, 'new title 2') + updatedNote = recreatedContext.items.findItem(note.uuid) + expect(updatedNote.signatureData).to.be.undefined + + await recreatedContext.deinit() + + recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) + await recreatedContext.launch() + + /** Decrypting from storage will now verify current user symmetric signature only */ + updatedNote = recreatedContext.items.findItem(note.uuid) + expect(updatedNote.signatureData.result.passes).to.equal(true) + + await recreatedContext.deinit() + await deinitContactContext() + }) + }) + + describe('symmetrically encrypted items', () => { + it('created items with a payload source of remote saved should not have signature data', async () => { + const note = await context.createSyncedNote() + + expect(note.payload.source).to.equal(PayloadSource.RemoteSaved) + + expect(note.signatureData).to.be.undefined + }) + + it('retrieved items that are then remote saved should have their signature data cleared', async () => { + const { note, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) + + await contactContext.changeNoteTitleAndSync(contactContext.items.findItem(note.uuid), 'new title') + + await context.sync() + expect(context.items.findItem(note.uuid).signatureData).to.not.be.undefined + + await context.changeNoteTitleAndSync(context.items.findItem(note.uuid), 'new title') + expect(context.items.findItem(note.uuid).signatureData).to.be.undefined + + await deinitContactContext() + }) + + it('should allow client verification of authenticity of shared item changes', async () => { + const { note, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) + + expect(context.contacts.isItemAuthenticallySigned(note)).to.equal('not-applicable') + + const contactNote = contactContext.items.findItem(note.uuid) + + expect(contactContext.contacts.isItemAuthenticallySigned(contactNote)).to.equal('yes') + + await contactContext.changeNoteTitleAndSync(contactNote, 'new title') + + await context.sync() + + let updatedNote = context.items.findItem(note.uuid) + + expect(context.contacts.isItemAuthenticallySigned(updatedNote)).to.equal('yes') + + await deinitContactContext() + }) + }) + + describe('keypair revocation', () => { + it('should be able to revoke non-current keypair', async () => { + console.error('TODO') + }) + + it('revoking a keypair should send a keypair revocation event to trusted contacts', async () => { + console.error('TODO') + }) + + it('should not be able to revoke current key pair', async () => { + console.error('TODO') + }) + + it('should distrust revoked keypair as contact', async () => { + console.error('TODO') + }) + }) +}) diff --git a/packages/snjs/mocha/vaults/deletion.test.js b/packages/snjs/mocha/vaults/deletion.test.js new file mode 100644 index 000000000..df6f4d825 --- /dev/null +++ b/packages/snjs/mocha/vaults/deletion.test.js @@ -0,0 +1,159 @@ +import * as Factory from '../lib/factory.js' +import * as Collaboration from '../lib/Collaboration.js' + +chai.use(chaiAsPromised) +const expect = chai.expect + +describe('shared vault deletion', function () { + this.timeout(Factory.TwentySecondTimeout) + + let context + let sharedVaults + + afterEach(async function () { + await context.deinit() + localStorage.clear() + }) + + beforeEach(async function () { + localStorage.clear() + + context = await Factory.createAppContextWithRealCrypto() + + await context.launch() + await context.register() + + sharedVaults = context.sharedVaults + }) + + it('should remove item from all user devices when item is deleted permanently', async () => { + const { note, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) + + const promise = context.resolveWhenSavedSyncPayloadsIncludesItemUuid(note.uuid) + await context.mutator.setItemToBeDeleted(note) + await context.sync() + await contactContext.sync() + await promise + + const originatorNote = context.items.findItem(note.uuid) + expect(originatorNote).to.be.undefined + + const collaboratorNote = contactContext.items.findItem(note.uuid) + expect(collaboratorNote).to.be.undefined + + await deinitContactContext() + }) + + it('attempting to delete a note received by and already deleted by another person should not cause infinite conflicts', async () => { + const { note, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) + + const promise = context.resolveWhenSavedSyncPayloadsIncludesItemUuid(note.uuid) + + await context.mutator.setItemToBeDeleted(note) + await contactContext.mutator.setItemToBeDeleted(note) + + await context.sync() + await contactContext.sync() + await promise + + const originatorNote = context.items.findItem(note.uuid) + expect(originatorNote).to.be.undefined + + const collaboratorNote = contactContext.items.findItem(note.uuid) + expect(collaboratorNote).to.be.undefined + + await deinitContactContext() + }) + + it('deleting a shared vault should remove all vault items from collaborator devices', async () => { + const { sharedVault, note, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) + + await sharedVaults.deleteSharedVault(sharedVault) + await contactContext.sync() + + const originatorNote = context.items.findItem(note.uuid) + expect(originatorNote).to.be.undefined + + const contactNote = contactContext.items.findItem(note.uuid) + expect(contactNote).to.be.undefined + + await deinitContactContext() + }) + + it('being removed from shared vault should remove shared vault items locally', async () => { + const { sharedVault, note, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) + + const contactNote = contactContext.items.findItem(note.uuid) + expect(contactNote).to.not.be.undefined + + await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid) + + await contactContext.sync() + + const updatedContactNote = contactContext.items.findItem(note.uuid) + expect(updatedContactNote).to.be.undefined + + await deinitContactContext() + }) + + it('leaving a shared vault should remove its items locally', async () => { + const { sharedVault, note, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context, SharedVaultPermission.Admin) + + const originalNote = contactContext.items.findItem(note.uuid) + expect(originalNote).to.not.be.undefined + + const contactVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) + await contactContext.sharedVaults.leaveSharedVault(contactVault) + + const updatedContactNote = contactContext.items.findItem(note.uuid) + expect(updatedContactNote).to.be.undefined + + const vault = await contactContext.vaults.getVault({ keySystemIdentifier: contactVault.systemIdentifier }) + expect(vault).to.be.undefined + + await deinitContactContext() + }) + + it('removing an item from a vault should remove it from collaborator devices', async () => { + const { note, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) + + await context.vaults.removeItemFromVault(note) + + await context.changeNoteTitleAndSync(note, 'new title') + + const receivedNote = contactContext.items.findItem(note.uuid) + + expect(receivedNote).to.not.be.undefined + expect(receivedNote.title).to.not.equal('new title') + expect(receivedNote.title).to.equal(note.title) + + await deinitContactContext() + }) + + it('should remove shared vault member', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + const originalSharedVaultUsers = await sharedVaults.getSharedVaultUsers(sharedVault) + expect(originalSharedVaultUsers.length).to.equal(2) + + const result = await sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid) + + expect(isClientDisplayableError(result)).to.be.false + + const updatedSharedVaultUsers = await sharedVaults.getSharedVaultUsers(sharedVault) + expect(updatedSharedVaultUsers.length).to.equal(1) + + await deinitContactContext() + }) + + it('being removed from a shared vault should delete respective vault listing', async () => { + console.error('TODO: implement test') + }) +}) diff --git a/packages/snjs/mocha/vaults/files.test.js b/packages/snjs/mocha/vaults/files.test.js new file mode 100644 index 000000000..5ee677e57 --- /dev/null +++ b/packages/snjs/mocha/vaults/files.test.js @@ -0,0 +1,259 @@ +import * as Factory from '../lib/factory.js' +import * as Files from '../lib/Files.js' +import * as Collaboration from '../lib/Collaboration.js' + +chai.use(chaiAsPromised) +const expect = chai.expect + +describe('shared vault files', function () { + this.timeout(Factory.TwentySecondTimeout) + + let context + let vaults + + afterEach(async function () { + await context.deinit() + localStorage.clear() + }) + + beforeEach(async function () { + localStorage.clear() + + context = await Factory.createAppContextWithRealCrypto() + + await context.launch() + await context.register() + + vaults = context.vaults + await context.publicMockSubscriptionPurchaseEvent() + }) + + describe('private vaults', () => { + it('should be able to upload and download file to vault as owner', async () => { + const vault = await Collaboration.createPrivateVault(context) + const response = await fetch('/mocha/assets/small_file.md') + const buffer = new Uint8Array(await response.arrayBuffer()) + const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, vault) + + const file = context.items.findItem(uploadedFile.uuid) + expect(file).to.not.be.undefined + expect(file.remoteIdentifier).to.equal(file.remoteIdentifier) + expect(file.key_system_identifier).to.equal(vault.systemIdentifier) + + const downloadedBytes = await Files.downloadFile(context.files, file) + expect(downloadedBytes).to.eql(buffer) + }) + }) + + it('should be able to upload and download file to vault as owner', async () => { + const sharedVault = await Collaboration.createSharedVault(context) + const response = await fetch('/mocha/assets/small_file.md') + const buffer = new Uint8Array(await response.arrayBuffer()) + const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault) + + const file = context.items.findItem(uploadedFile.uuid) + expect(file).to.not.be.undefined + expect(file.remoteIdentifier).to.equal(file.remoteIdentifier) + expect(file.key_system_identifier).to.equal(sharedVault.systemIdentifier) + + const downloadedBytes = await Files.downloadFile(context.files, file) + expect(downloadedBytes).to.eql(buffer) + }) + + it('should be able to move a user file to a vault', async () => { + const response = await fetch('/mocha/assets/small_file.md') + const buffer = new Uint8Array(await response.arrayBuffer()) + + const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000) + + const sharedVault = await Collaboration.createSharedVault(context) + const addedFile = await vaults.moveItemToVault(sharedVault, uploadedFile) + + const downloadedBytes = await Files.downloadFile(context.files, addedFile) + expect(downloadedBytes).to.eql(buffer) + }) + + it('should be able to move a shared vault file to another shared vault', async () => { + const response = await fetch('/mocha/assets/small_file.md') + const buffer = new Uint8Array(await response.arrayBuffer()) + + const firstVault = await Collaboration.createSharedVault(context) + const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, firstVault) + + const secondVault = await Collaboration.createSharedVault(context) + const movedFile = await vaults.moveItemToVault(secondVault, uploadedFile) + + const downloadedBytes = await Files.downloadFile(context.files, movedFile) + expect(downloadedBytes).to.eql(buffer) + }) + + it('should be able to move a shared vault file to a non-shared vault', async () => { + const response = await fetch('/mocha/assets/small_file.md') + const buffer = new Uint8Array(await response.arrayBuffer()) + + const firstVault = await Collaboration.createSharedVault(context) + const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, firstVault) + const privateVault = await Collaboration.createPrivateVault(context) + + const addedFile = await vaults.moveItemToVault(privateVault, uploadedFile) + + const downloadedBytes = await Files.downloadFile(context.files, addedFile) + expect(downloadedBytes).to.eql(buffer) + }) + + it('moving a note to a vault should also moved linked files', async () => { + const note = await context.createSyncedNote() + const response = await fetch('/mocha/assets/small_file.md') + const buffer = new Uint8Array(await response.arrayBuffer()) + const file = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000) + + const updatedFile = await context.application.mutator.associateFileWithNote(file, note) + + const sharedVault = await Collaboration.createSharedVault(context) + + vaults.alerts.confirmV2 = () => Promise.resolve(true) + + await vaults.moveItemToVault(sharedVault, note) + + const latestFile = context.items.findItem(updatedFile.uuid) + + expect(vaults.getItemVault(latestFile).uuid).to.equal(sharedVault.uuid) + expect(vaults.getItemVault(context.items.findItem(note.uuid)).uuid).to.equal(sharedVault.uuid) + + const downloadedBytes = await Files.downloadFile(context.files, latestFile) + expect(downloadedBytes).to.eql(buffer) + }) + + it('should be able to move a file out of its vault', async () => { + const response = await fetch('/mocha/assets/small_file.md') + const buffer = new Uint8Array(await response.arrayBuffer()) + + const sharedVault = await Collaboration.createSharedVault(context) + const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault) + + const removedFile = await vaults.removeItemFromVault(uploadedFile) + expect(removedFile.key_system_identifier).to.not.be.ok + + const downloadedBytes = await Files.downloadFile(context.files, removedFile) + expect(downloadedBytes).to.eql(buffer) + }) + + it('should be able to download vault file as collaborator', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + const response = await fetch('/mocha/assets/small_file.md') + const buffer = new Uint8Array(await response.arrayBuffer()) + const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault) + + await contactContext.sync() + + const sharedFile = contactContext.items.findItem(uploadedFile.uuid) + expect(sharedFile).to.not.be.undefined + expect(sharedFile.remoteIdentifier).to.equal(uploadedFile.remoteIdentifier) + + const downloadedBytes = await Files.downloadFile(contactContext.files, sharedFile) + expect(downloadedBytes).to.eql(buffer) + + await deinitContactContext() + }) + + it('should be able to upload vault file as collaborator', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + const response = await fetch('/mocha/assets/small_file.md') + const buffer = new Uint8Array(await response.arrayBuffer()) + + const uploadedFile = await Files.uploadFile(contactContext.files, buffer, 'my-file', 'md', 1000, sharedVault) + + await context.sync() + + const file = context.items.findItem(uploadedFile.uuid) + expect(file).to.not.be.undefined + expect(file.remoteIdentifier).to.equal(file.remoteIdentifier) + + const downloadedBytes = await Files.downloadFile(context.files, file) + expect(downloadedBytes).to.eql(buffer) + + await deinitContactContext() + }) + + it('should be able to delete vault file as write user', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context, SharedVaultPermission.Write) + const response = await fetch('/mocha/assets/small_file.md') + const buffer = new Uint8Array(await response.arrayBuffer()) + + const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault) + + await contactContext.sync() + + const file = contactContext.items.findItem(uploadedFile.uuid) + const result = await contactContext.files.deleteFile(file) + expect(result).to.be.undefined + + const foundFile = contactContext.items.findItem(file.uuid) + expect(foundFile).to.be.undefined + + await deinitContactContext() + }) + + it('should not be able to delete vault file as read user', async () => { + context.anticipateConsoleError('Could not create valet token') + + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context, SharedVaultPermission.Read) + const response = await fetch('/mocha/assets/small_file.md') + const buffer = new Uint8Array(await response.arrayBuffer()) + + const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault) + + await contactContext.sync() + + const file = contactContext.items.findItem(uploadedFile.uuid) + const result = await contactContext.files.deleteFile(file) + expect(isClientDisplayableError(result)).to.be.true + + const foundFile = contactContext.items.findItem(file.uuid) + expect(foundFile).to.not.be.undefined + + await deinitContactContext() + }) + + it('should be able to download recently moved vault file as collaborator', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + const response = await fetch('/mocha/assets/small_file.md') + const buffer = new Uint8Array(await response.arrayBuffer()) + const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000) + const addedFile = await vaults.moveItemToVault(sharedVault, uploadedFile) + + await contactContext.sync() + + const sharedFile = contactContext.items.findItem(addedFile.uuid) + expect(sharedFile).to.not.be.undefined + expect(sharedFile.remoteIdentifier).to.equal(addedFile.remoteIdentifier) + + const downloadedBytes = await Files.downloadFile(contactContext.files, sharedFile) + expect(downloadedBytes).to.eql(buffer) + + await deinitContactContext() + }) + + it('should not be able to download file after being removed from vault', async () => { + context.anticipateConsoleError('Could not create valet token') + + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + const response = await fetch('/mocha/assets/small_file.md') + const buffer = new Uint8Array(await response.arrayBuffer()) + const uploadedFile = await Files.uploadFile(context.files, buffer, 'my-file', 'md', 1000, sharedVault) + await contactContext.sync() + + await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid) + + const file = contactContext.items.findItem(uploadedFile.uuid) + await Factory.expectThrowsAsync(() => Files.downloadFile(contactContext.files, file), 'Could not download file') + + await deinitContactContext() + }) +}) diff --git a/packages/snjs/mocha/vaults/invites.test.js b/packages/snjs/mocha/vaults/invites.test.js new file mode 100644 index 000000000..4071af7b6 --- /dev/null +++ b/packages/snjs/mocha/vaults/invites.test.js @@ -0,0 +1,229 @@ +import * as Factory from '../lib/factory.js' +import * as Collaboration from '../lib/Collaboration.js' + +chai.use(chaiAsPromised) +const expect = chai.expect + +describe('shared vault invites', function () { + this.timeout(Factory.TwentySecondTimeout) + + let context + let sharedVaults + + afterEach(async function () { + await context.deinit() + localStorage.clear() + }) + + beforeEach(async function () { + localStorage.clear() + + context = await Factory.createAppContextWithRealCrypto() + await context.launch() + await context.register() + + sharedVaults = context.sharedVaults + }) + + it('should invite contact to vault', async () => { + const sharedVault = await Collaboration.createSharedVault(context) + const { contactContext, deinitContactContext } = await Collaboration.createContactContext() + const contact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext) + + const vaultInvite = await sharedVaults.inviteContactToSharedVault(sharedVault, contact, SharedVaultPermission.Write) + + expect(vaultInvite).to.not.be.undefined + expect(vaultInvite.shared_vault_uuid).to.equal(sharedVault.sharing.sharedVaultUuid) + expect(vaultInvite.user_uuid).to.equal(contact.contactUuid) + expect(vaultInvite.encrypted_message).to.not.be.undefined + expect(vaultInvite.permissions).to.equal(SharedVaultPermission.Write) + expect(vaultInvite.updated_at_timestamp).to.not.be.undefined + expect(vaultInvite.created_at_timestamp).to.not.be.undefined + + await deinitContactContext() + }) + + it('invites from trusted contact should be pending as trusted', async () => { + const { contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithUnacceptedButTrustedInvite(context) + + const invites = contactContext.sharedVaults.getCachedPendingInviteRecords() + + expect(invites[0].trusted).to.be.true + + await deinitContactContext() + }) + + it('invites from untrusted contact should be pending as untrusted', async () => { + const { contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithUnacceptedAndUntrustedInvite(context) + + const invites = contactContext.sharedVaults.getCachedPendingInviteRecords() + + expect(invites[0].trusted).to.be.false + + await deinitContactContext() + }) + + it('invite should include delegated trusted contacts', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault( + context, + sharedVault, + ) + + const invites = thirdPartyContext.sharedVaults.getCachedPendingInviteRecords() + + const message = invites[0].message + const delegatedContacts = message.data.trustedContacts + expect(delegatedContacts.length).to.equal(1) + expect(delegatedContacts[0].contactUuid).to.equal(contactContext.userUuid) + + await deinitThirdPartyContext() + await deinitContactContext() + }) + + it('should sync a shared vault from scratch after accepting an invitation', async () => { + const sharedVault = await Collaboration.createSharedVault(context) + + const note = await context.createSyncedNote('foo', 'bar') + await Collaboration.moveItemToVault(context, sharedVault, note) + + /** Create a mutually trusted contact */ + const { contactContext, deinitContactContext } = await Collaboration.createContactContext() + const contact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext) + await Collaboration.createTrustedContactForUserOfContext(contactContext, context) + + /** Sync the contact context so that they wouldn't naturally receive changes made before this point */ + await contactContext.sync() + + await sharedVaults.inviteContactToSharedVault(sharedVault, contact, SharedVaultPermission.Write) + + /** Contact should now sync and expect to find note */ + const promise = contactContext.awaitNextSyncSharedVaultFromScratchEvent() + await contactContext.sync() + await Collaboration.acceptAllInvites(contactContext) + await promise + + const receivedNote = contactContext.items.findItem(note.uuid) + expect(receivedNote).to.not.be.undefined + expect(receivedNote.title).to.equal('foo') + expect(receivedNote.text).to.equal(note.text) + + await deinitContactContext() + }) + + it('received invites from untrusted contact should not be trusted', async () => { + await context.createSyncedNote('foo', 'bar') + const { contactContext, deinitContactContext } = await Collaboration.createContactContext() + const sharedVault = await Collaboration.createSharedVault(context) + + const currentContextContact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext) + await sharedVaults.inviteContactToSharedVault(sharedVault, currentContextContact, SharedVaultPermission.Write) + + await contactContext.sharedVaults.downloadInboundInvites() + expect(contactContext.sharedVaults.getCachedPendingInviteRecords()[0].trusted).to.be.false + + await deinitContactContext() + }) + + it('received invites from contact who becomes trusted after receipt of invite should be trusted', async () => { + await context.createSyncedNote('foo', 'bar') + const { contactContext, deinitContactContext } = await Collaboration.createContactContext() + const sharedVault = await Collaboration.createSharedVault(context) + + const currentContextContact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext) + await sharedVaults.inviteContactToSharedVault(sharedVault, currentContextContact, SharedVaultPermission.Write) + + await contactContext.sharedVaults.downloadInboundInvites() + expect(contactContext.sharedVaults.getCachedPendingInviteRecords()[0].trusted).to.be.false + + await Collaboration.createTrustedContactForUserOfContext(contactContext, context) + + expect(contactContext.sharedVaults.getCachedPendingInviteRecords()[0].trusted).to.be.true + + await deinitContactContext() + }) + + it('received items should contain the uuid of the contact who sent the item', async () => { + const { note, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) + + const receivedNote = contactContext.items.findItem(note.uuid) + expect(receivedNote).to.not.be.undefined + expect(receivedNote.user_uuid).to.equal(context.userUuid) + + await deinitContactContext() + }) + + it('items should contain the uuid of the last person who edited it', async () => { + const { note, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) + + const receivedNote = contactContext.items.findItem(note.uuid) + expect(receivedNote.last_edited_by_uuid).to.not.be.undefined + expect(receivedNote.last_edited_by_uuid).to.equal(context.userUuid) + + await contactContext.changeNoteTitleAndSync(receivedNote, 'new title') + await context.sync() + + const updatedNote = context.items.findItem(note.uuid) + expect(updatedNote.last_edited_by_uuid).to.not.be.undefined + expect(updatedNote.last_edited_by_uuid).to.equal(contactContext.userUuid) + + await deinitContactContext() + }) + + it('canceling an invite should remove it from recipient pending invites', async () => { + const { invite, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithUnacceptedButTrustedInvite(context) + + const preInvites = await contactContext.sharedVaults.downloadInboundInvites() + expect(preInvites.length).to.equal(1) + + await sharedVaults.deleteInvite(invite) + + const postInvites = await contactContext.sharedVaults.downloadInboundInvites() + expect(postInvites.length).to.equal(0) + + await deinitContactContext() + }) + + it('when inviter keypair changes, recipient should still be able to trust and decrypt previous invite', async () => { + console.error('TODO: implement test') + }) + + it('should delete all inbound invites after changing user password', async () => { + /** Invites to user are encrypted with old keypair and are no longer decryptable */ + console.error('TODO: implement test') + }) + + it('sharing a vault with user inputted and ephemeral password should share the key as synced for the recipient', async () => { + const privateVault = await context.vaults.createUserInputtedPasswordVault({ + name: 'My Private Vault', + userInputtedPassword: 'password', + storagePreference: KeySystemRootKeyStorageMode.Ephemeral, + }) + + const note = await context.createSyncedNote('foo', 'bar') + await context.vaults.moveItemToVault(privateVault, note) + + const sharedVault = await context.sharedVaults.convertVaultToSharedVault(privateVault) + + const { thirdPartyContext, deinitThirdPartyContext } = await Collaboration.inviteNewPartyToSharedVault( + context, + sharedVault, + ) + + await Collaboration.acceptAllInvites(thirdPartyContext) + + const contextNote = thirdPartyContext.items.findItem(note.uuid) + expect(contextNote).to.not.be.undefined + expect(contextNote.title).to.equal('foo') + expect(contextNote.text).to.equal(note.text) + + await deinitThirdPartyContext() + }) +}) diff --git a/packages/snjs/mocha/vaults/items.test.js b/packages/snjs/mocha/vaults/items.test.js new file mode 100644 index 000000000..6df81b384 --- /dev/null +++ b/packages/snjs/mocha/vaults/items.test.js @@ -0,0 +1,121 @@ +import * as Factory from '../lib/factory.js' +import * as Collaboration from '../lib/Collaboration.js' + +chai.use(chaiAsPromised) +const expect = chai.expect + +describe('shared vault items', function () { + this.timeout(Factory.TwentySecondTimeout) + + let context + let sharedVaults + + afterEach(async function () { + await context.deinit() + localStorage.clear() + }) + + beforeEach(async function () { + localStorage.clear() + + context = await Factory.createAppContextWithRealCrypto() + + await context.launch() + await context.register() + + sharedVaults = context.sharedVaults + }) + + it('should add item to shared vault with no other members', async () => { + const note = await context.createSyncedNote('foo', 'bar') + + const sharedVault = await Collaboration.createSharedVault(context) + + await Collaboration.moveItemToVault(context, sharedVault, note) + + const updatedNote = context.items.findItem(note.uuid) + expect(updatedNote.key_system_identifier).to.equal(sharedVault.systemIdentifier) + expect(updatedNote.shared_vault_uuid).to.equal(sharedVault.sharing.sharedVaultUuid) + }) + + it('should add item to shared vault with contact', async () => { + const note = await context.createSyncedNote('foo', 'bar') + + const { sharedVault, deinitContactContext } = await Collaboration.createSharedVaultWithAcceptedInvite(context) + + await Collaboration.moveItemToVault(context, sharedVault, note) + + const updatedNote = context.items.findItem(note.uuid) + expect(updatedNote.key_system_identifier).to.equal(sharedVault.systemIdentifier) + + await deinitContactContext() + }) + + it('received items from previously trusted contact should be decrypted', async () => { + const note = await context.createSyncedNote('foo', 'bar') + const { contactContext, deinitContactContext } = await Collaboration.createContactContext() + const sharedVault = await Collaboration.createSharedVault(context) + + await Collaboration.createTrustedContactForUserOfContext(contactContext, context) + const currentContextContact = await Collaboration.createTrustedContactForUserOfContext(context, contactContext) + + contactContext.lockSyncing() + await sharedVaults.inviteContactToSharedVault(sharedVault, currentContextContact, SharedVaultPermission.Write) + await Collaboration.moveItemToVault(context, sharedVault, note) + + const promise = contactContext.awaitNextSyncSharedVaultFromScratchEvent() + contactContext.unlockSyncing() + await contactContext.sync() + await Collaboration.acceptAllInvites(contactContext) + await promise + + const receivedItemsKey = contactContext.keys.getPrimaryKeySystemItemsKey(sharedVault.systemIdentifier) + expect(receivedItemsKey).to.not.be.undefined + expect(receivedItemsKey.itemsKey).to.not.be.undefined + + const receivedNote = contactContext.items.findItem(note.uuid) + expect(receivedNote.title).to.equal('foo') + expect(receivedNote.text).to.equal(note.text) + + await deinitContactContext() + }) + + it('shared vault creator should receive changes from other members', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + const note = await context.createSyncedNote('foo', 'bar') + await Collaboration.moveItemToVault(context, sharedVault, note) + await contactContext.sync() + + await contactContext.mutator.changeItem({ uuid: note.uuid }, (mutator) => { + mutator.title = 'new title' + }) + await contactContext.sync() + await context.sync() + + const receivedNote = context.items.findItem(note.uuid) + expect(receivedNote.title).to.equal('new title') + + await deinitContactContext() + }) + + it('items added by collaborator should be received by shared vault owner', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) + + const newNote = await contactContext.createSyncedNote('new note', 'new note text') + await Collaboration.moveItemToVault(contactContext, sharedVault, newNote) + + await context.sync() + + const receivedNote = context.items.findItem(newNote.uuid) + expect(receivedNote).to.not.be.undefined + expect(receivedNote.title).to.equal('new note') + + await deinitContactContext() + }) + + it('adding item to vault while belonging to other vault should move the item to new vault', async () => { + console.error('TODO: implement test') + }) +}) diff --git a/packages/snjs/mocha/vaults/key_rotation.test.js b/packages/snjs/mocha/vaults/key_rotation.test.js new file mode 100644 index 000000000..ff45df94c --- /dev/null +++ b/packages/snjs/mocha/vaults/key_rotation.test.js @@ -0,0 +1,269 @@ +import * as Factory from '../lib/factory.js' +import * as Collaboration from '../lib/Collaboration.js' + +chai.use(chaiAsPromised) +const expect = chai.expect + +describe('shared vault key rotation', function () { + this.timeout(Factory.TwentySecondTimeout) + + let context + let vaults + let sharedVaults + + afterEach(async function () { + await context.deinit() + localStorage.clear() + }) + + beforeEach(async function () { + localStorage.clear() + + context = await Factory.createAppContextWithRealCrypto() + + await context.launch() + await context.register() + + vaults = context.vaults + sharedVaults = context.sharedVaults + }) + + it('should reencrypt all items keys belonging to key system', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + contactContext.lockSyncing() + + const spy = sinon.spy(context.encryption, 'reencryptKeySystemItemsKeysForVault') + + const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault) + await vaults.rotateVaultRootKey(sharedVault) + await promise + + expect(spy.callCount).to.equal(1) + + deinitContactContext() + }) + + it("rotating a vault's key should send an asymmetric message to all members", async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + contactContext.lockSyncing() + + const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault) + await vaults.rotateVaultRootKey(sharedVault) + await promise + + const outboundMessages = await context.asymmetric.getOutboundMessages() + const expectedMessages = ['root key change', 'vault metadata change'] + expect(outboundMessages.length).to.equal(expectedMessages.length) + + const message = outboundMessages[0] + expect(message).to.not.be.undefined + expect(message.user_uuid).to.equal(contactContext.userUuid) + expect(message.encrypted_message).to.not.be.undefined + + await deinitContactContext() + }) + + it('should update recipient vault display listing with new key params', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + contactContext.anticipateConsoleError( + '(2x) Error decrypting contentKey from parameters', + 'Items keys are encrypted with new root key and are later decrypted in the test', + ) + + contactContext.lockSyncing() + + const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault) + await vaults.rotateVaultRootKey(sharedVault) + await promise + + const rootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier) + + contactContext.unlockSyncing() + await contactContext.sync() + + const vault = await contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) + expect(vault.rootKeyParams).to.eql(rootKey.keyParams) + + await deinitContactContext() + }) + + it('should receive new key system items key', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + contactContext.anticipateConsoleError( + '(2x) Error decrypting contentKey from parameters', + 'Items keys are encrypted with new root key and are later decrypted in the test', + ) + contactContext.lockSyncing() + + const previousPrimaryItemsKey = contactContext.keys.getPrimaryKeySystemItemsKey(sharedVault.systemIdentifier) + expect(previousPrimaryItemsKey).to.not.be.undefined + + const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault) + await vaults.rotateVaultRootKey(sharedVault) + await promise + + const contactPromise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() + contactContext.unlockSyncing() + await contactContext.sync() + await contactPromise + + const newPrimaryItemsKey = contactContext.keys.getPrimaryKeySystemItemsKey(sharedVault.systemIdentifier) + expect(newPrimaryItemsKey).to.not.be.undefined + + expect(newPrimaryItemsKey.uuid).to.not.equal(previousPrimaryItemsKey.uuid) + expect(newPrimaryItemsKey.itemsKey).to.not.eql(previousPrimaryItemsKey.itemsKey) + + await deinitContactContext() + }) + + it("rotating a vault's key with a pending invite should create new invite and delete old", async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithUnacceptedButTrustedInvite(context) + contactContext.lockSyncing() + + const originalOutboundInvites = await sharedVaults.getOutboundInvites() + expect(originalOutboundInvites.length).to.equal(1) + const originalInviteMessage = originalOutboundInvites[0].encrypted_message + + const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault) + await vaults.rotateVaultRootKey(sharedVault) + await promise + + const updatedOutboundInvites = await sharedVaults.getOutboundInvites() + expect(updatedOutboundInvites.length).to.equal(1) + + const joinInvite = updatedOutboundInvites[0] + expect(joinInvite.encrypted_message).to.not.be.undefined + expect(joinInvite.encrypted_message).to.not.equal(originalInviteMessage) + + await deinitContactContext() + }) + + it('new key system items key in rotated shared vault should belong to shared vault', async () => { + const sharedVault = await Collaboration.createSharedVault(context) + + await vaults.rotateVaultRootKey(sharedVault) + + const keySystemItemsKeys = context.keys + .getAllKeySystemItemsKeys() + .filter((key) => key.key_system_identifier === sharedVault.systemIdentifier) + + expect(keySystemItemsKeys.length).to.equal(2) + + for (const key of keySystemItemsKeys) { + expect(key.shared_vault_uuid).to.equal(sharedVault.sharing.sharedVaultUuid) + } + }) + + it('should update existing key-change messages instead of creating new ones', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + contactContext.lockSyncing() + + const firstPromise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault) + await vaults.rotateVaultRootKey(sharedVault) + await firstPromise + + const asymmetricMessageAfterFirstChange = await context.asymmetric.getOutboundMessages() + const expectedMessages = ['root key change', 'vault metadata change'] + expect(asymmetricMessageAfterFirstChange.length).to.equal(expectedMessages.length) + + const messageAfterFirstChange = asymmetricMessageAfterFirstChange[0] + + const secondPromise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault) + await vaults.rotateVaultRootKey(sharedVault) + await secondPromise + + const asymmetricMessageAfterSecondChange = await context.asymmetric.getOutboundMessages() + expect(asymmetricMessageAfterSecondChange.length).to.equal(expectedMessages.length) + + const messageAfterSecondChange = asymmetricMessageAfterSecondChange[0] + expect(messageAfterSecondChange.encrypted_message).to.not.equal(messageAfterFirstChange.encrypted_message) + expect(messageAfterSecondChange.uuid).to.not.equal(messageAfterFirstChange.uuid) + + await deinitContactContext() + }) + + it('key change messages should be automatically processed by trusted contacts', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + contactContext.anticipateConsoleError( + '(2x) Error decrypting contentKey from parameters', + 'Items keys are encrypted with new root key and are later decrypted in the test', + ) + contactContext.lockSyncing() + + const promise = context.resolveWhenSharedVaultKeyRotationInvitesGetSent(sharedVault) + await vaults.rotateVaultRootKey(sharedVault) + await promise + + const acceptMessage = sinon.spy(contactContext.asymmetric, 'handleTrustedSharedVaultRootKeyChangedMessage') + + contactContext.unlockSyncing() + await contactContext.sync() + + expect(acceptMessage.callCount).to.equal(1) + + await deinitContactContext() + }) + + it('should rotate key system root key after removing vault member', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + const originalKeySystemRootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier) + + await sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid) + + const newKeySystemRootKey = context.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier) + + expect(newKeySystemRootKey.keyParams.creationTimestamp).to.be.greaterThan( + originalKeySystemRootKey.keyParams.creationTimestamp, + ) + expect(newKeySystemRootKey.key).to.not.equal(originalKeySystemRootKey.key) + + await deinitContactContext() + }) + + it('should throw if attempting to change password of locked vault', async () => { + console.error('TODO: implement') + }) + + it('should respect storage preference when rotating key system root key', async () => { + console.error('TODO: implement') + }) + + it('should change storage preference from synced to local', async () => { + console.error('TODO: implement') + }) + + it('should change storage preference from local to synced', async () => { + console.error('TODO: implement') + }) + + it('should resync key system items key if it is encrypted with noncurrent key system root key', async () => { + console.error('TODO: implement') + }) + + it('should change password type from user inputted to randomized', async () => { + console.error('TODO: implement') + }) + + it('should change password type from randomized to user inputted', async () => { + console.error('TODO: implement') + }) + + it('should not be able to change storage mode of third party vault', async () => { + console.error('TODO: implement') + }) +}) diff --git a/packages/snjs/mocha/vaults/permissions.test.js b/packages/snjs/mocha/vaults/permissions.test.js new file mode 100644 index 000000000..81d475449 --- /dev/null +++ b/packages/snjs/mocha/vaults/permissions.test.js @@ -0,0 +1,133 @@ +import * as Factory from '../lib/factory.js' +import * as Collaboration from '../lib/Collaboration.js' + +chai.use(chaiAsPromised) +const expect = chai.expect + +describe('shared vault permissions', function () { + this.timeout(Factory.TwentySecondTimeout) + + let context + let sharedVaults + + afterEach(async function () { + await context.deinit() + localStorage.clear() + }) + + beforeEach(async function () { + localStorage.clear() + + context = await Factory.createAppContextWithRealCrypto() + + await context.launch() + await context.register() + + sharedVaults = context.sharedVaults + }) + + it('non-admin user should not be able to invite user', async () => { + context.anticipateConsoleError('Could not create invite') + + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInviteAndNote(context) + + const thirdParty = await Collaboration.createContactContext() + const thirdPartyContact = await Collaboration.createTrustedContactForUserOfContext( + contactContext, + thirdParty.contactContext, + ) + const result = await contactContext.sharedVaults.inviteContactToSharedVault( + sharedVault, + thirdPartyContact, + SharedVaultPermission.Write, + ) + + expect(isClientDisplayableError(result)).to.be.true + + await deinitContactContext() + }) + + it('should not be able to leave shared vault as creator', async () => { + context.anticipateConsoleError('Could not delete user') + + const sharedVault = await Collaboration.createSharedVault(context) + + const result = await sharedVaults.removeUserFromSharedVault(sharedVault, context.userUuid) + + expect(isClientDisplayableError(result)).to.be.true + }) + + it('should be able to leave shared vault as added admin', async () => { + const { contactVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context, SharedVaultPermission.Admin) + + const result = await contactContext.sharedVaults.leaveSharedVault(contactVault) + + expect(isClientDisplayableError(result)).to.be.false + + await deinitContactContext() + }) + + it('non-admin user should not be able to create or update vault items keys with the server', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + const keySystemItemsKey = contactContext.keys.getKeySystemItemsKeys(sharedVault.systemIdentifier)[0] + + await contactContext.mutator.changeItem(keySystemItemsKey, () => {}) + const promise = contactContext.resolveWithConflicts() + await contactContext.sync() + + const conflicts = await promise + + expect(conflicts.length).to.equal(1) + expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.KeySystemItemsKey) + + await deinitContactContext() + }) + + it('read user should not be able to make changes to items', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context, SharedVaultPermission.Read) + const note = await context.createSyncedNote('foo', 'bar') + await Collaboration.moveItemToVault(context, sharedVault, note) + await contactContext.sync() + + await contactContext.mutator.changeItem({ uuid: note.uuid }, (mutator) => { + mutator.title = 'new title' + }) + + const promise = contactContext.resolveWithConflicts() + await contactContext.sync() + const conflicts = await promise + + expect(conflicts.length).to.equal(1) + expect(conflicts[0].unsaved_item.content_type).to.equal(ContentType.Note) + + await deinitContactContext() + }) + + it('should be able to move item from vault to user as a write user if the item belongs to me', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + const note = await contactContext.createSyncedNote('foo', 'bar') + await Collaboration.moveItemToVault(contactContext, sharedVault, note) + await contactContext.sync() + + const promise = contactContext.resolveWithConflicts() + await contactContext.vaults.removeItemFromVault(note) + const conflicts = await promise + + expect(conflicts.length).to.equal(0) + + const duplicateNote = contactContext.findDuplicateNote(note.uuid) + expect(duplicateNote).to.be.undefined + + const existingNote = contactContext.items.findItem(note.uuid) + expect(existingNote.key_system_identifier).to.not.be.ok + + await deinitContactContext() + }) +}) diff --git a/packages/snjs/mocha/vaults/pkc.test.js b/packages/snjs/mocha/vaults/pkc.test.js new file mode 100644 index 000000000..4a431d22d --- /dev/null +++ b/packages/snjs/mocha/vaults/pkc.test.js @@ -0,0 +1,98 @@ +import * as Factory from '../lib/factory.js' + +chai.use(chaiAsPromised) +const expect = chai.expect + +describe('public key cryptography', function () { + this.timeout(Factory.TwentySecondTimeout) + + let context + let sessions + let encryption + + afterEach(async function () { + await context.deinit() + localStorage.clear() + }) + + beforeEach(async function () { + localStorage.clear() + + context = await Factory.createAppContextWithRealCrypto() + + await context.launch() + await context.register() + + sessions = context.application.sessions + encryption = context.encryption + }) + + it('should create keypair during registration', () => { + expect(sessions.getPublicKey()).to.not.be.undefined + expect(encryption.getKeyPair().privateKey).to.not.be.undefined + + expect(sessions.getSigningPublicKey()).to.not.be.undefined + expect(encryption.getSigningKeyPair().privateKey).to.not.be.undefined + }) + + it('should populate keypair during sign in', async () => { + const email = context.email + const password = context.password + await context.signout() + + const recreatedContext = await Factory.createAppContextWithRealCrypto() + await recreatedContext.launch() + recreatedContext.email = email + recreatedContext.password = password + await recreatedContext.signIn() + + expect(recreatedContext.sessions.getPublicKey()).to.not.be.undefined + expect(recreatedContext.encryption.getKeyPair().privateKey).to.not.be.undefined + + expect(recreatedContext.sessions.getSigningPublicKey()).to.not.be.undefined + expect(recreatedContext.encryption.getSigningKeyPair().privateKey).to.not.be.undefined + }) + + it('should rotate keypair during password change', async () => { + const oldPublicKey = sessions.getPublicKey() + const oldPrivateKey = encryption.getKeyPair().privateKey + + const oldSigningPublicKey = sessions.getSigningPublicKey() + const oldSigningPrivateKey = encryption.getSigningKeyPair().privateKey + + await context.changePassword('new_password') + + expect(sessions.getPublicKey()).to.not.be.undefined + expect(encryption.getKeyPair().privateKey).to.not.be.undefined + expect(sessions.getPublicKey()).to.not.equal(oldPublicKey) + expect(encryption.getKeyPair().privateKey).to.not.equal(oldPrivateKey) + + expect(sessions.getSigningPublicKey()).to.not.be.undefined + expect(encryption.getSigningKeyPair().privateKey).to.not.be.undefined + expect(sessions.getSigningPublicKey()).to.not.equal(oldSigningPublicKey) + expect(encryption.getSigningKeyPair().privateKey).to.not.equal(oldSigningPrivateKey) + }) + + it('should allow option to enable collaboration for previously signed in accounts', async () => { + const newContext = await Factory.createAppContextWithRealCrypto() + await newContext.launch() + + await newContext.register() + + const rootKey = await newContext.encryption.getRootKey() + const mutatedRootKey = CreateNewRootKey({ + ...rootKey.content, + encryptionKeyPair: undefined, + signingKeyPair: undefined, + }) + + await newContext.encryption.setRootKey(mutatedRootKey) + + expect(newContext.application.sessions.isUserMissingKeyPair()).to.be.true + + const result = await newContext.application.user.updateAccountWithFirstTimeKeyPair() + expect(result.error).to.be.undefined + + expect(newContext.application.sessions.isUserMissingKeyPair()).to.be.false + }) +}) diff --git a/packages/snjs/mocha/vaults/shared_vaults.test.js b/packages/snjs/mocha/vaults/shared_vaults.test.js new file mode 100644 index 000000000..13bb987f7 --- /dev/null +++ b/packages/snjs/mocha/vaults/shared_vaults.test.js @@ -0,0 +1,114 @@ +import * as Factory from '../lib/factory.js' +import * as Collaboration from '../lib/Collaboration.js' + +chai.use(chaiAsPromised) +const expect = chai.expect + +describe('shared vaults', function () { + this.timeout(Factory.TwentySecondTimeout) + + let context + let vaults + + afterEach(async function () { + await context.deinit() + localStorage.clear() + }) + + beforeEach(async function () { + localStorage.clear() + + context = await Factory.createAppContextWithRealCrypto() + + await context.launch() + await context.register() + + vaults = context.vaults + }) + + it('should update vault name and description', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + await vaults.changeVaultNameAndDescription(sharedVault, { + name: 'new vault name', + description: 'new vault description', + }) + + const updatedVault = vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) + expect(updatedVault.name).to.equal('new vault name') + expect(updatedVault.description).to.equal('new vault description') + + const promise = contactContext.resolveWhenAsymmetricMessageProcessingCompletes() + await contactContext.sync() + await promise + + const contactVault = contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier }) + expect(contactVault.name).to.equal('new vault name') + expect(contactVault.description).to.equal('new vault description') + + await deinitContactContext() + }) + + it('being removed from a shared vault should remove the vault', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + const result = await context.sharedVaults.removeUserFromSharedVault(sharedVault, contactContext.userUuid) + + expect(result).to.be.undefined + + const promise = contactContext.resolveWhenUserMessagesProcessingCompletes() + await contactContext.sync() + await promise + + expect(contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })).to.be.undefined + expect(contactContext.encryption.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)).to.be.undefined + expect(contactContext.encryption.keys.getKeySystemItemsKeys(sharedVault.systemIdentifier)).to.be.empty + + const recreatedContext = await Factory.createAppContextWithRealCrypto(contactContext.identifier) + await recreatedContext.launch() + + expect(recreatedContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })).to.be.undefined + expect(recreatedContext.encryption.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)).to.be.undefined + expect(recreatedContext.encryption.keys.getKeySystemItemsKeys(sharedVault.systemIdentifier)).to.be.empty + + await deinitContactContext() + await recreatedContext.deinit() + }) + + it('deleting a shared vault should remove vault from contact context', async () => { + const { sharedVault, contactContext, deinitContactContext } = + await Collaboration.createSharedVaultWithAcceptedInvite(context) + + const result = await context.sharedVaults.deleteSharedVault(sharedVault) + + expect(result).to.be.undefined + + const promise = contactContext.resolveWhenUserMessagesProcessingCompletes() + await contactContext.sync() + await promise + + expect(contactContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })).to.be.undefined + expect(contactContext.encryption.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)).to.be.undefined + expect(contactContext.encryption.keys.getKeySystemItemsKeys(sharedVault.systemIdentifier)).to.be.empty + + const recreatedContext = await Factory.createAppContextWithRealCrypto(contactContext.identifier) + await recreatedContext.launch() + + expect(recreatedContext.vaults.getVault({ keySystemIdentifier: sharedVault.systemIdentifier })).to.be.undefined + expect(recreatedContext.encryption.keys.getPrimaryKeySystemRootKey(sharedVault.systemIdentifier)).to.be.undefined + expect(recreatedContext.encryption.keys.getKeySystemItemsKeys(sharedVault.systemIdentifier)).to.be.empty + + await deinitContactContext() + await recreatedContext.deinit() + }) + + it('should convert a vault to a shared vault', async () => { + console.error('TODO') + }) + + it('should send metadata change message when changing name or description', async () => { + console.error('TODO') + }) +}) diff --git a/packages/snjs/mocha/vaults/vaults.test.js b/packages/snjs/mocha/vaults/vaults.test.js new file mode 100644 index 000000000..4f2fad311 --- /dev/null +++ b/packages/snjs/mocha/vaults/vaults.test.js @@ -0,0 +1,250 @@ +import * as Factory from '../lib/factory.js' + +chai.use(chaiAsPromised) +const expect = chai.expect + +describe('vaults', function () { + this.timeout(Factory.TwentySecondTimeout) + + let context + let vaults + + afterEach(async function () { + await context.deinit() + localStorage.clear() + }) + + beforeEach(async function () { + localStorage.clear() + + context = await Factory.createAppContextWithRealCrypto() + + await context.launch() + + vaults = context.vaults + }) + + describe('locking', () => { + it('should throw if attempting to add item to locked vault', async () => { + console.error('TODO: implement') + }) + + it('should throw if attempting to remove item from locked vault', async () => { + console.error('TODO: implement') + }) + + it('locking vault should remove root key and items keys from memory', async () => { + console.error('TODO: implement') + }) + }) + + describe('offline', function () { + it('should be able to create an offline vault', async () => { + const vault = await vaults.createRandomizedVault({ + name: 'My Vault', + storagePreference: KeySystemRootKeyStorageMode.Synced, + }) + + expect(vault.systemIdentifier).to.not.be.undefined + expect(typeof vault.systemIdentifier).to.equal('string') + + const keySystemItemsKey = context.keys.getPrimaryKeySystemItemsKey(vault.systemIdentifier) + expect(keySystemItemsKey).to.not.be.undefined + expect(keySystemItemsKey.key_system_identifier).to.equal(vault.systemIdentifier) + expect(keySystemItemsKey.creationTimestamp).to.not.be.undefined + expect(keySystemItemsKey.keyVersion).to.not.be.undefined + }) + + it('should be able to create an offline vault with app passcode', async () => { + await context.application.addPasscode('123') + const vault = await vaults.createRandomizedVault({ + name: 'My Vault', + storagePreference: KeySystemRootKeyStorageMode.Synced, + }) + + expect(vault.systemIdentifier).to.not.be.undefined + expect(typeof vault.systemIdentifier).to.equal('string') + + const keySystemItemsKey = context.keys.getPrimaryKeySystemItemsKey(vault.systemIdentifier) + expect(keySystemItemsKey).to.not.be.undefined + expect(keySystemItemsKey.key_system_identifier).to.equal(vault.systemIdentifier) + expect(keySystemItemsKey.creationTimestamp).to.not.be.undefined + expect(keySystemItemsKey.keyVersion).to.not.be.undefined + }) + + it('should add item to offline vault', async () => { + const vault = await vaults.createRandomizedVault({ + name: 'My Vault', + storagePreference: KeySystemRootKeyStorageMode.Synced, + }) + const item = await context.createSyncedNote() + + await vaults.moveItemToVault(vault, item) + + const updatedItem = context.items.findItem(item.uuid) + expect(updatedItem.key_system_identifier).to.equal(vault.systemIdentifier) + }) + + it('should load data in the correct order at startup to allow vault items and their keys to decrypt', async () => { + const appIdentifier = context.identifier + const vault = await vaults.createRandomizedVault({ + name: 'My Vault', + storagePreference: KeySystemRootKeyStorageMode.Synced, + }) + const note = await context.createSyncedNote('foo', 'bar') + await vaults.moveItemToVault(vault, note) + await context.deinit() + + const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) + await recreatedContext.launch() + + const updatedNote = recreatedContext.items.findItem(note.uuid) + expect(updatedNote.title).to.equal('foo') + expect(updatedNote.text).to.equal('bar') + + await recreatedContext.deinit() + }) + + describe('porting from offline to online', () => { + it('should maintain vault system identifiers across items after registration', async () => { + const appIdentifier = context.identifier + const vault = await vaults.createRandomizedVault({ + name: 'My Vault', + storagePreference: KeySystemRootKeyStorageMode.Synced, + }) + const note = await context.createSyncedNote('foo', 'bar') + await vaults.moveItemToVault(vault, note) + + await context.register() + await context.sync() + + await context.deinit() + + const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) + await recreatedContext.launch() + + const notes = recreatedContext.notes + expect(notes.length).to.equal(1) + + const updatedNote = recreatedContext.items.findItem(note.uuid) + expect(updatedNote.title).to.equal('foo') + expect(updatedNote.text).to.equal('bar') + expect(updatedNote.key_system_identifier).to.equal(vault.systemIdentifier) + + await recreatedContext.deinit() + }) + + it('should decrypt vault items', async () => { + const appIdentifier = context.identifier + const vault = await vaults.createRandomizedVault({ + name: 'My Vault', + storagePreference: KeySystemRootKeyStorageMode.Synced, + }) + const note = await context.createSyncedNote('foo', 'bar') + await vaults.moveItemToVault(vault, note) + + await context.register() + await context.sync() + + await context.deinit() + + const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) + await recreatedContext.launch() + + const updatedNote = recreatedContext.items.findItem(note.uuid) + expect(updatedNote.title).to.equal('foo') + expect(updatedNote.text).to.equal('bar') + + await recreatedContext.deinit() + }) + }) + }) + + describe('online', () => { + beforeEach(async () => { + await context.register() + }) + + it('should create a vault', async () => { + const vault = await vaults.createRandomizedVault({ + name: 'My Vault', + storagePreference: KeySystemRootKeyStorageMode.Synced, + }) + expect(vault).to.not.be.undefined + + const keySystemItemsKeys = context.keys.getKeySystemItemsKeys(vault.systemIdentifier) + expect(keySystemItemsKeys.length).to.equal(1) + + const keySystemItemsKey = keySystemItemsKeys[0] + expect(keySystemItemsKey instanceof KeySystemItemsKey).to.be.true + expect(keySystemItemsKey.key_system_identifier).to.equal(vault.systemIdentifier) + }) + + it('should add item to vault', async () => { + const note = await context.createSyncedNote('foo', 'bar') + const vault = await vaults.createRandomizedVault({ + name: 'My Vault', + storagePreference: KeySystemRootKeyStorageMode.Synced, + }) + + await vaults.moveItemToVault(vault, note) + + const updatedNote = context.items.findItem(note.uuid) + expect(updatedNote.key_system_identifier).to.equal(vault.systemIdentifier) + }) + + describe('client timing', () => { + it('should load data in the correct order at startup to allow vault items and their keys to decrypt', async () => { + const appIdentifier = context.identifier + const vault = await vaults.createRandomizedVault({ + name: 'My Vault', + storagePreference: KeySystemRootKeyStorageMode.Synced, + }) + const note = await context.createSyncedNote('foo', 'bar') + await vaults.moveItemToVault(vault, note) + await context.deinit() + + const recreatedContext = await Factory.createAppContextWithRealCrypto(appIdentifier) + await recreatedContext.launch() + + const updatedNote = recreatedContext.items.findItem(note.uuid) + expect(updatedNote.title).to.equal('foo') + expect(updatedNote.text).to.equal('bar') + + await recreatedContext.deinit() + }) + }) + + describe('key system root key rotation', () => { + it('rotating a key system root key should create a new vault items key', async () => { + const vault = await vaults.createRandomizedVault({ + name: 'My Vault', + storagePreference: KeySystemRootKeyStorageMode.Synced, + }) + + const keySystemItemsKey = context.keys.getKeySystemItemsKeys(vault.systemIdentifier)[0] + + await vaults.rotateVaultRootKey(vault) + + const updatedKeySystemItemsKey = context.keys.getKeySystemItemsKeys(vault.systemIdentifier)[0] + + expect(updatedKeySystemItemsKey).to.not.be.undefined + expect(updatedKeySystemItemsKey.uuid).to.not.equal(keySystemItemsKey.uuid) + }) + + it('deleting a vault should delete all its items', async () => { + const vault = await vaults.createRandomizedVault({ + name: 'My Vault', + storagePreference: KeySystemRootKeyStorageMode.Synced, + }) + const note = await context.createSyncedNote('foo', 'bar') + await vaults.moveItemToVault(vault, note) + + await vaults.deleteVault(vault) + + const updatedNote = context.items.findItem(note.uuid) + expect(updatedNote).to.be.undefined + }) + }) + }) +}) diff --git a/packages/snjs/package.json b/packages/snjs/package.json index 6fcf8a032..7a9dc3667 100644 --- a/packages/snjs/package.json +++ b/packages/snjs/package.json @@ -1,6 +1,6 @@ { "name": "@standardnotes/snjs", - "version": "2.169.6", + "version": "2.200.0", "engines": { "node": ">=16.0.0 <17.0.0" }, @@ -36,7 +36,7 @@ "@babel/core": "*", "@babel/preset-env": "*", "@standardnotes/api": "workspace:*", - "@standardnotes/common": "^1.46.6", + "@standardnotes/common": "^1.48.3", "@standardnotes/domain-core": "^1.12.0", "@standardnotes/domain-events": "^2.108.1", "@standardnotes/encryption": "workspace:*", diff --git a/packages/snjs/webpack.dev.js b/packages/snjs/webpack.dev.js index 94c481407..845157139 100644 --- a/packages/snjs/webpack.dev.js +++ b/packages/snjs/webpack.dev.js @@ -1,5 +1,6 @@ -const { merge } = require('webpack-merge'); -const config = require('./webpack.config.js'); +const { merge } = require('webpack-merge') +const config = require('./webpack.config.js') +const webpack = require('webpack') module.exports = merge(config, { mode: 'development', @@ -7,4 +8,9 @@ module.exports = merge(config, { stats: { colors: true, }, -}); + plugins: [ + new webpack.DefinePlugin({ + __IS_DEV__: true, + }), + ], +}) diff --git a/packages/snjs/webpack.prod.js b/packages/snjs/webpack.prod.js index 5d991fbaa..4db463c24 100644 --- a/packages/snjs/webpack.prod.js +++ b/packages/snjs/webpack.prod.js @@ -1,6 +1,12 @@ -const { merge } = require('webpack-merge'); -const config = require('./webpack.config.js'); +const { merge } = require('webpack-merge') +const config = require('./webpack.config.js') +const webpack = require('webpack') module.exports = merge(config, { mode: 'production', -}); + plugins: [ + new webpack.DefinePlugin({ + __IS_DEV__: false, + }), + ], +}) diff --git a/packages/ui-services/package.json b/packages/ui-services/package.json index 8da022518..acd78775f 100644 --- a/packages/ui-services/package.json +++ b/packages/ui-services/package.json @@ -15,13 +15,16 @@ "test": "jest spec" }, "dependencies": { - "@standardnotes/common": "^1.46.6", + "@standardnotes/common": "^1.48.3", "@standardnotes/features": "workspace:^", "@standardnotes/filepicker": "workspace:^", + "@standardnotes/models": "workspace:^", "@standardnotes/services": "workspace:^", "@standardnotes/styles": "workspace:^", "@standardnotes/toast": "workspace:^", - "@standardnotes/utils": "workspace:^" + "@standardnotes/utils": "workspace:^", + "mobx": "^6.8.0", + "mobx-react-lite": "^3.4.2" }, "devDependencies": { "@types/jest": "^29.2.3", diff --git a/packages/ui-services/src/Abstract/AbstractUIService.ts b/packages/ui-services/src/Abstract/AbstractUIService.ts new file mode 100644 index 000000000..4818bc8e7 --- /dev/null +++ b/packages/ui-services/src/Abstract/AbstractUIService.ts @@ -0,0 +1,52 @@ +import { AbstractService, InternalEventBusInterface, ApplicationEvent } from '@standardnotes/services' +import { AbstractUIServiceInterface } from './AbstractUIServiceInterface' +import { WebApplicationInterface } from '../WebApplication/WebApplicationInterface' + +export class AbstractUIServicee + extends AbstractService + implements AbstractUIServiceInterface +{ + private unsubApp!: () => void + + constructor( + protected application: WebApplicationInterface, + protected override internalEventBus: InternalEventBusInterface, + ) { + super(internalEventBus) + this.addAppEventObserverAfterSubclassesFinishConstructing() + } + + async onAppStart() { + return + } + + async onAppEvent(_event: ApplicationEvent) { + return + } + + private addAppEventObserverAfterSubclassesFinishConstructing() { + setTimeout(() => { + this.addAppEventObserver() + }, 0) + } + + private addAppEventObserver() { + if (this.application.isStarted()) { + void this.onAppStart() + } + + this.unsubApp = this.application.addEventObserver(async (event: ApplicationEvent) => { + await this.onAppEvent(event) + if (event === ApplicationEvent.Started) { + void this.onAppStart() + } + }) + } + + override deinit() { + ;(this.application as unknown) = undefined + this.unsubApp() + ;(this.unsubApp as unknown) = undefined + super.deinit() + } +} diff --git a/packages/ui-services/src/Abstract/AbstractUIServiceInterface.ts b/packages/ui-services/src/Abstract/AbstractUIServiceInterface.ts new file mode 100644 index 000000000..4f97f56d7 --- /dev/null +++ b/packages/ui-services/src/Abstract/AbstractUIServiceInterface.ts @@ -0,0 +1,7 @@ +import { ApplicationEvent, ServiceInterface } from '@standardnotes/services' + +export interface AbstractUIServiceInterface + extends ServiceInterface { + onAppStart(): Promise + onAppEvent(event: ApplicationEvent): Promise +} diff --git a/packages/ui-services/src/Alert/WebAlertService.ts b/packages/ui-services/src/Alert/WebAlertService.ts index 0483efc1a..52ef93692 100644 --- a/packages/ui-services/src/Alert/WebAlertService.ts +++ b/packages/ui-services/src/Alert/WebAlertService.ts @@ -4,6 +4,30 @@ import { SKAlert } from '@standardnotes/styles' import { alertDialog, confirmDialog } from './Functions' export class WebAlertService extends AlertService { + override confirmV2(dto: { + text: string + title?: string | undefined + confirmButtonText?: string | undefined + confirmButtonType?: ButtonType | undefined + cancelButtonText?: string | undefined + }): Promise { + return confirmDialog({ + text: dto.text, + title: dto.title, + confirmButtonText: dto.confirmButtonText, + cancelButtonText: dto.cancelButtonText, + confirmButtonStyle: dto.confirmButtonType === ButtonType.Danger ? 'danger' : 'info', + }) + } + + override alertV2(dto: { + text: string + title?: string | undefined + closeButtonText?: string | undefined + }): Promise { + return alertDialog({ text: dto.text, title: dto.title, closeButtonText: dto.closeButtonText }) + } + alert(text: string, title?: string, closeButtonText?: string) { return alertDialog({ text, title, closeButtonText }) } diff --git a/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.spec.ts b/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.spec.ts index 29b197819..7b6a033b0 100644 --- a/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.spec.ts +++ b/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.spec.ts @@ -1,5 +1,5 @@ +import { WebApplicationInterface } from './../../WebApplication/WebApplicationInterface' import { FeatureIdentifier, NoteType } from '@standardnotes/features' -import { WebApplicationInterface } from '@standardnotes/services' import { AegisToAuthenticatorConverter } from './AegisToAuthenticatorConverter' import data from './testData' diff --git a/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.ts b/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.ts index 45c62f2e8..1c062621a 100644 --- a/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.ts +++ b/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.ts @@ -2,7 +2,7 @@ import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' import { ContentType } from '@standardnotes/common' import { readFileAsText } from '../Utils' import { FeatureIdentifier, NoteType } from '@standardnotes/features' -import { WebApplicationInterface } from '@standardnotes/services' +import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' type AegisData = { db: { diff --git a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts index 6a3d7830d..4a6626c3a 100644 --- a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts +++ b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts @@ -3,10 +3,10 @@ */ import { ContentType } from '@standardnotes/common' -import { WebApplicationInterface } from '@standardnotes/services' import { DecryptedTransferPayload, NoteContent, TagContent } from '@standardnotes/models' import { EvernoteConverter } from './EvernoteConverter' import data from './testData' +import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' // Mock dayjs so dayjs.extend() doesn't throw an error in EvernoteConverter.ts jest.mock('dayjs', () => { diff --git a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts index 6d5309c91..da9a136cd 100644 --- a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts +++ b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts @@ -1,10 +1,10 @@ import { ContentType } from '@standardnotes/common' -import { WebApplicationInterface } from '@standardnotes/services' import { DecryptedTransferPayload, NoteContent, TagContent } from '@standardnotes/models' import { readFileAsText } from '../Utils' import dayjs from 'dayjs' import customParseFormat from 'dayjs/plugin/customParseFormat' import utc from 'dayjs/plugin/utc' +import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' dayjs.extend(customParseFormat) dayjs.extend(utc) diff --git a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts index 632b9c4df..cd973ef54 100644 --- a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts +++ b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts @@ -2,9 +2,9 @@ * @jest-environment jsdom */ -import { WebApplicationInterface } from '@standardnotes/snjs' import { jsonTestData, htmlTestData } from './testData' import { GoogleKeepConverter } from './GoogleKeepConverter' +import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' describe('GoogleKeepConverter', () => { let application: WebApplicationInterface diff --git a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts index 0be6cd0d4..fae0963f3 100644 --- a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts +++ b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts @@ -1,7 +1,7 @@ -import { WebApplicationInterface } from '@standardnotes/services' import { ContentType } from '@standardnotes/common' import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' import { readFileAsText } from '../Utils' +import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' type GoogleKeepJsonNote = { color: string diff --git a/packages/ui-services/src/Import/Importer.ts b/packages/ui-services/src/Import/Importer.ts index cd0ab8006..d5ce20d8a 100644 --- a/packages/ui-services/src/Import/Importer.ts +++ b/packages/ui-services/src/Import/Importer.ts @@ -1,5 +1,5 @@ import { parseFileName } from '@standardnotes/filepicker' -import { FeatureStatus, WebApplicationInterface } from '@standardnotes/services' +import { FeatureStatus } from '@standardnotes/services' import { FeatureIdentifier } from '@standardnotes/features' import { AegisToAuthenticatorConverter } from './AegisConverter/AegisToAuthenticatorConverter' import { EvernoteConverter } from './EvernoteConverter/EvernoteConverter' @@ -8,6 +8,7 @@ import { PlaintextConverter } from './PlaintextConverter/PlaintextConverter' import { SimplenoteConverter } from './SimplenoteConverter/SimplenoteConverter' import { readFileAsText } from './Utils' import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' +import { WebApplicationInterface } from '../WebApplication/WebApplicationInterface' export type NoteImportType = 'plaintext' | 'evernote' | 'google-keep' | 'simplenote' | 'aegis' @@ -82,7 +83,7 @@ export class Importer { const insertedItems = await Promise.all( payloads.map(async (payload) => { const content = payload.content as NoteContent - const note = this.application.mutator.createTemplateItem( + const note = this.application.items.createTemplateItem( payload.content_type, { text: content.text, diff --git a/packages/ui-services/src/Import/PlaintextConverter/PlaintextConverter.ts b/packages/ui-services/src/Import/PlaintextConverter/PlaintextConverter.ts index c132dd23a..76220f3e4 100644 --- a/packages/ui-services/src/Import/PlaintextConverter/PlaintextConverter.ts +++ b/packages/ui-services/src/Import/PlaintextConverter/PlaintextConverter.ts @@ -1,8 +1,8 @@ import { ContentType } from '@standardnotes/common' import { parseFileName } from '@standardnotes/filepicker' import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' -import { WebApplicationInterface } from '@standardnotes/services' import { readFileAsText } from '../Utils' +import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' export class PlaintextConverter { constructor(protected application: WebApplicationInterface) {} diff --git a/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.spec.ts b/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.spec.ts index e540ae701..a5a622d09 100644 --- a/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.spec.ts +++ b/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.spec.ts @@ -1,4 +1,4 @@ -import { WebApplicationInterface } from '@standardnotes/services' +import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' import { SimplenoteConverter } from './SimplenoteConverter' import data from './testData' diff --git a/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.ts b/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.ts index 8b3ffd4b5..6ce4076bc 100644 --- a/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.ts +++ b/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.ts @@ -1,7 +1,7 @@ import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' import { ContentType } from '@standardnotes/common' import { readFileAsText } from '../Utils' -import { WebApplicationInterface } from '@standardnotes/services' +import { WebApplicationInterface } from '../../WebApplication/WebApplicationInterface' type SimplenoteItem = { creationDate: string diff --git a/packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts b/packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts index 684694d16..d1fc2699a 100644 --- a/packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts +++ b/packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts @@ -1,4 +1,4 @@ -import { Environment, Platform } from '@standardnotes/snjs' +import { Environment, Platform } from '@standardnotes/models' import { isMacPlatform } from './platformCheck' import { CREATE_NEW_NOTE_KEYBOARD_COMMAND, diff --git a/packages/ui-services/src/Preferences/PreferenceId.ts b/packages/ui-services/src/Preferences/PreferenceId.ts index 923de784d..07933f60a 100644 --- a/packages/ui-services/src/Preferences/PreferenceId.ts +++ b/packages/ui-services/src/Preferences/PreferenceId.ts @@ -2,6 +2,7 @@ const PREFERENCE_IDS = [ 'general', 'account', 'security', + 'vaults', 'appearance', 'backups', 'listed', diff --git a/packages/ui-services/src/Theme/ThemeManager.ts b/packages/ui-services/src/Theme/ThemeManager.ts index 411582ee5..389f7d46c 100644 --- a/packages/ui-services/src/Theme/ThemeManager.ts +++ b/packages/ui-services/src/Theme/ThemeManager.ts @@ -8,41 +8,31 @@ import { SNTheme, } from '@standardnotes/models' import { removeFromArray } from '@standardnotes/utils' -import { - AbstractService, - WebApplicationInterface, - InternalEventBusInterface, - ApplicationEvent, - StorageValueModes, - FeatureStatus, -} from '@standardnotes/services' +import { InternalEventBusInterface, ApplicationEvent, StorageValueModes, FeatureStatus } from '@standardnotes/services' import { FeatureIdentifier } from '@standardnotes/features' +import { WebApplicationInterface } from '../WebApplication/WebApplicationInterface' +import { AbstractUIServicee } from '../Abstract/AbstractUIService' const CachedThemesKey = 'cachedThemes' const TimeBeforeApplyingColorScheme = 5 const DefaultThemeIdentifier = 'Default' -export class ThemeManager extends AbstractService { +export class ThemeManager extends AbstractUIServicee { private activeThemes: string[] = [] private unregisterDesktop?: () => void private unregisterStream!: () => void private lastUseDeviceThemeSettings = false - private unsubApp!: () => void - constructor( - protected application: WebApplicationInterface, - protected override internalEventBus: InternalEventBusInterface, - ) { - super(internalEventBus) - this.addAppEventObserverAfterSubclassesFinishConstructing() + constructor(application: WebApplicationInterface, internalEventBus: InternalEventBusInterface) { + super(application, internalEventBus) this.colorSchemeEventHandler = this.colorSchemeEventHandler.bind(this) } - async onAppStart() { + override async onAppStart() { this.registerObservers() } - async onAppEvent(event: ApplicationEvent) { + override async onAppEvent(event: ApplicationEvent) { switch (event) { case ApplicationEvent.SignedOut: { this.deactivateAllThemes() @@ -76,25 +66,6 @@ export class ThemeManager extends AbstractService { } } - addAppEventObserverAfterSubclassesFinishConstructing() { - setTimeout(() => { - this.addAppEventObserver() - }, 0) - } - - addAppEventObserver() { - if (this.application.isStarted()) { - void this.onAppStart() - } - - this.unsubApp = this.application.addEventObserver(async (event: ApplicationEvent) => { - await this.onAppEvent(event) - if (event === ApplicationEvent.Started) { - void this.onAppStart() - } - }) - } - async handleMobileColorSchemeChangeEvent() { const useDeviceThemeSettings = this.application.getPreference(PrefKey.UseSystemColorScheme, false) @@ -124,10 +95,6 @@ export class ThemeManager extends AbstractService { } } - get webApplication() { - return this.application as WebApplicationInterface - } - override deinit() { this.activeThemes.length = 0 @@ -143,11 +110,6 @@ export class ThemeManager extends AbstractService { mq.removeListener(this.colorSchemeEventHandler) } - ;(this.application as unknown) = undefined - - this.unsubApp() - ;(this.unsubApp as unknown) = undefined - super.deinit() } @@ -166,7 +128,7 @@ export class ThemeManager extends AbstractService { const status = this.application.features.getFeatureStatus(theme.identifier) if (status !== FeatureStatus.Entitled) { if (theme.active) { - this.application.mutator.toggleTheme(theme).catch(console.error) + this.application.componentManager.toggleTheme(theme.uuid).catch(console.error) } else { this.deactivateTheme(theme.uuid) } @@ -242,7 +204,7 @@ export class ThemeManager extends AbstractService { const toggleActiveTheme = () => { if (activeTheme) { - void this.application.mutator.toggleTheme(activeTheme) + void this.application.componentManager.toggleTheme(activeTheme.uuid) } } @@ -252,7 +214,7 @@ export class ThemeManager extends AbstractService { } else { const theme = themes.find((theme) => theme.package_info.identifier === themeIdentifier) if (theme && !theme.active) { - this.application.mutator.toggleTheme(theme).catch(console.error) + this.application.componentManager.toggleTheme(theme.uuid).catch(console.error) } } } @@ -272,7 +234,7 @@ export class ThemeManager extends AbstractService { } private registerObservers() { - this.unregisterDesktop = this.webApplication.getDesktopService()?.registerUpdateObserver((component) => { + this.unregisterDesktop = this.application.getDesktopService()?.registerUpdateObserver((component) => { if (component.active && component.isTheme()) { this.deactivateTheme(component.uuid) setTimeout(() => { diff --git a/packages/ui-services/src/Vaults/VaultDisplayService.ts b/packages/ui-services/src/Vaults/VaultDisplayService.ts new file mode 100644 index 000000000..d02a23911 --- /dev/null +++ b/packages/ui-services/src/Vaults/VaultDisplayService.ts @@ -0,0 +1,216 @@ +import { + ApplicationEvent, + ApplicationStage, + ApplicationStageChangedEventPayload, + Challenge, + ChallengePrompt, + ChallengeReason, + ChallengeStrings, + ChallengeValidation, + InternalEventBusInterface, + InternalEventHandlerInterface, + InternalEventInterface, + StorageKey, + VaultServiceEvent, +} from '@standardnotes/services' +import { VaultDisplayOptions, VaultDisplayOptionsPersistable, VaultListingInterface } from '@standardnotes/models' +import { VaultDisplayServiceEvent } from './VaultDisplayServiceEvent' +import { AbstractUIServicee } from '../Abstract/AbstractUIService' +import { WebApplicationInterface } from '../WebApplication/WebApplicationInterface' +import { VaultDisplayServiceInterface } from './VaultDisplayServiceInterface' +import { action, makeObservable, observable } from 'mobx' + +export class VaultDisplayService + extends AbstractUIServicee + implements VaultDisplayServiceInterface, InternalEventHandlerInterface +{ + options: VaultDisplayOptions + + public exclusivelyShownVault: VaultListingInterface | undefined = undefined + + constructor(application: WebApplicationInterface, internalEventBus: InternalEventBusInterface) { + super(application, internalEventBus) + + this.options = new VaultDisplayOptions({ exclude: [], locked: [] }) + + internalEventBus.addEventHandler(this, VaultServiceEvent.VaultLocked) + internalEventBus.addEventHandler(this, VaultServiceEvent.VaultUnlocked) + internalEventBus.addEventHandler(this, ApplicationEvent.ApplicationStageChanged) + + makeObservable(this, { + options: observable, + + isVaultExplicitelyExcluded: observable, + isVaultExclusivelyShown: observable, + exclusivelyShownVault: observable, + + hideVault: action, + unhideVault: action, + showOnlyVault: action, + }) + } + + async handleEvent(event: InternalEventInterface): Promise { + if (event.type === VaultServiceEvent.VaultLocked || event.type === VaultServiceEvent.VaultUnlocked) { + this.handleVaultLockingStatusChanged() + } else if (event.type === ApplicationEvent.ApplicationStageChanged) { + const stage = (event.payload as ApplicationStageChangedEventPayload).stage + if (stage === ApplicationStage.StorageDecrypted_09) { + void this.loadVaultSelectionOptionsFromDisk() + } + } + } + + private handleVaultLockingStatusChanged(): void { + const lockedVaults = this.application.vaults.getLockedvaults() + + const options = this.options.newOptionsByIntakingLockedVaults(lockedVaults) + this.setVaultSelectionOptions(options) + } + + public getOptions(): VaultDisplayOptions { + return this.options + } + + isVaultExplicitelyExcluded = (vault: VaultListingInterface): boolean => { + return this.options.isVaultExplicitelyExcluded(vault) ?? false + } + + isVaultDisabledOrLocked(vault: VaultListingInterface): boolean { + return this.options.isVaultDisabledOrLocked(vault) + } + + isVaultExclusivelyShown = (vault: VaultListingInterface): boolean => { + return this.options.isVaultExclusivelyShown(vault) + } + + isInExclusiveDisplayMode(): boolean { + return this.options.isInExclusiveDisplayMode() + } + + changeToMultipleVaultDisplayMode(): void { + const vaults = this.application.vaults.getVaults() + const lockedVaults = this.application.vaults.getLockedvaults() + + const newOptions = new VaultDisplayOptions({ + exclude: vaults + .map((vault) => vault.systemIdentifier) + .filter((identifier) => identifier !== this.exclusivelyShownVault?.systemIdentifier), + locked: lockedVaults.map((vault) => vault.systemIdentifier), + }) + + this.setVaultSelectionOptions(newOptions) + } + + hideVault = (vault: VaultListingInterface) => { + const lockedVaults = this.application.vaults.getLockedvaults() + const newOptions = this.options.newOptionsByExcludingVault(vault, lockedVaults) + this.setVaultSelectionOptions(newOptions) + } + + unhideVault = async (vault: VaultListingInterface) => { + if (this.application.vaults.isVaultLocked(vault)) { + const unlocked = await this.unlockVault(vault) + if (!unlocked) { + return + } + } + + const lockedVaults = this.application.vaults.getLockedvaults() + const newOptions = this.options.newOptionsByUnexcludingVault(vault, lockedVaults) + this.setVaultSelectionOptions(newOptions) + } + + showOnlyVault = async (vault: VaultListingInterface) => { + if (this.application.vaults.isVaultLocked(vault)) { + const unlocked = await this.unlockVault(vault) + if (!unlocked) { + return + } + } + + const newOptions = new VaultDisplayOptions({ exclusive: vault.systemIdentifier }) + this.setVaultSelectionOptions(newOptions) + } + + async unlockVault(vault: VaultListingInterface): Promise { + if (!this.application.vaults.isVaultLocked(vault)) { + throw new Error('Attempting to unlock a vault that is not locked.') + } + + const challenge = new Challenge( + [new ChallengePrompt(ChallengeValidation.None, undefined, 'Password')], + ChallengeReason.Custom, + true, + ChallengeStrings.UnlockVault(vault.name), + ChallengeStrings.EnterVaultPassword, + ) + + return new Promise((resolve) => { + this.application.challenges.addChallengeObserver(challenge, { + onCancel() { + resolve(false) + }, + onNonvalidatedSubmit: async (challengeResponse) => { + const value = challengeResponse.getDefaultValue() + if (!value) { + this.application.challenges.completeChallenge(challenge) + resolve(false) + return + } + + const password = value.value as string + + const unlocked = await this.application.vaults.unlockNonPersistentVault(vault, password) + if (!unlocked) { + this.application.challenges.setValidationStatusForChallenge(challenge, value, false) + resolve(false) + return + } + + this.application.challenges.completeChallenge(challenge) + resolve(true) + }, + }) + + void this.application.challenges.promptForChallengeResponse(challenge) + }) + } + + private setVaultSelectionOptions = (options: VaultDisplayOptions) => { + this.options = options + + if (this.isInExclusiveDisplayMode()) { + this.exclusivelyShownVault = this.application.vaults.getVault({ + keySystemIdentifier: this.options.getExclusivelyShownVault(), + }) + } else { + this.exclusivelyShownVault = undefined + } + + this.application.items.setVaultDisplayOptions(options) + + void this.notifyEvent(VaultDisplayServiceEvent.VaultDisplayOptionsChanged, options) + + if (this.application.isLaunched()) { + this.application.setValue(StorageKey.VaultSelectionOptions, options.getPersistableValue()) + } + } + + private loadVaultSelectionOptionsFromDisk = (): void => { + const raw = this.application.getValue(StorageKey.VaultSelectionOptions) + if (!raw) { + return + } + + const options = VaultDisplayOptions.FromPersistableValue(raw) + + this.options = options + void this.notifyEvent(VaultDisplayServiceEvent.VaultDisplayOptionsChanged, options) + } + + override deinit(): void { + ;(this.options as unknown) = undefined + super.deinit() + } +} diff --git a/packages/ui-services/src/Vaults/VaultDisplayServiceEvent.ts b/packages/ui-services/src/Vaults/VaultDisplayServiceEvent.ts new file mode 100644 index 000000000..754dd0d79 --- /dev/null +++ b/packages/ui-services/src/Vaults/VaultDisplayServiceEvent.ts @@ -0,0 +1,3 @@ +export enum VaultDisplayServiceEvent { + VaultDisplayOptionsChanged = 'VaultDisplayOptionsChanged', +} diff --git a/packages/ui-services/src/Vaults/VaultDisplayServiceInterface.ts b/packages/ui-services/src/Vaults/VaultDisplayServiceInterface.ts new file mode 100644 index 000000000..00993679d --- /dev/null +++ b/packages/ui-services/src/Vaults/VaultDisplayServiceInterface.ts @@ -0,0 +1,20 @@ +import { VaultDisplayOptions, VaultListingInterface } from '@standardnotes/models' +import { AbstractUIServiceInterface } from '../Abstract/AbstractUIServiceInterface' + +export interface VaultDisplayServiceInterface extends AbstractUIServiceInterface { + exclusivelyShownVault?: VaultListingInterface + + getOptions(): VaultDisplayOptions + + isVaultDisabledOrLocked(vault: VaultListingInterface): boolean + isVaultExplicitelyExcluded: (vault: VaultListingInterface) => boolean + isVaultExclusivelyShown: (vault: VaultListingInterface) => boolean + isInExclusiveDisplayMode(): boolean + + changeToMultipleVaultDisplayMode(): void + + hideVault: (vault: VaultListingInterface) => void + unhideVault: (vault: VaultListingInterface) => void + showOnlyVault: (vault: VaultListingInterface) => void + unlockVault(vault: VaultListingInterface): Promise +} diff --git a/packages/services/src/Domain/Application/WebApplicationInterface.ts b/packages/ui-services/src/WebApplication/WebApplicationInterface.ts similarity index 78% rename from packages/services/src/Domain/Application/WebApplicationInterface.ts rename to packages/ui-services/src/WebApplication/WebApplicationInterface.ts index 9395f59d4..79f7a8aac 100644 --- a/packages/services/src/Domain/Application/WebApplicationInterface.ts +++ b/packages/ui-services/src/WebApplication/WebApplicationInterface.ts @@ -1,7 +1,9 @@ -import { MobileDeviceInterface } from './../Device/MobileDeviceInterface' -import { DesktopManagerInterface } from '../Device/DesktopManagerInterface' -import { WebAppEvent } from '../Event/WebAppEvent' -import { ApplicationInterface } from './ApplicationInterface' +import { + ApplicationInterface, + DesktopManagerInterface, + MobileDeviceInterface, + WebAppEvent, +} from '@standardnotes/services' export interface WebApplicationInterface extends ApplicationInterface { notifyWebEvent(event: WebAppEvent, data?: unknown): void diff --git a/packages/ui-services/src/index.ts b/packages/ui-services/src/index.ts index 9e397f7df..b64941b7d 100644 --- a/packages/ui-services/src/index.ts +++ b/packages/ui-services/src/index.ts @@ -33,3 +33,9 @@ export * from './Toast/ToastService' export * from './Toast/ToastServiceInterface' export * from './StatePersistence/StatePersistence' export * from './Import' + +export * from './Vaults/VaultDisplayService' +export * from './Vaults/VaultDisplayServiceEvent' +export * from './Vaults/VaultDisplayServiceInterface' + +export * from './WebApplication/WebApplicationInterface' diff --git a/packages/utils/package.json b/packages/utils/package.json index ee98286ca..dfb1ea2ca 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -25,7 +25,7 @@ "test": "jest spec" }, "dependencies": { - "@standardnotes/common": "^1.46.6", + "@standardnotes/common": "^1.48.3", "dompurify": "^2.4.1", "lodash": "^4.17.21", "reflect-metadata": "^0.1.13" diff --git a/packages/web/package.json b/packages/web/package.json index f3c79021d..4ea9f98ae 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -16,7 +16,7 @@ "lint:fix": "eslint src/javascripts --fix", "start": "webpack-dev-server --config web.webpack.dev.js", "start-secure": "yarn start --server-type https", - "test": "jest --config jest.config.js --coverage", + "test": "jest --config jest.config.js", "tsc": "tsc --project tsconfig.json", "upgrade:snjs": "ncu -u '@standardnotes/*'", "watch": "webpack -w --config web.webpack.dev.js", diff --git a/packages/web/src/javascripts/Application/Device/DesktopManager.ts b/packages/web/src/javascripts/Application/Device/DesktopManager.ts index e1cb86cf3..d4027c099 100644 --- a/packages/web/src/javascripts/Application/Device/DesktopManager.ts +++ b/packages/web/src/javascripts/Application/Device/DesktopManager.ts @@ -13,11 +13,11 @@ import { assert, DesktopClientRequiresWebMethods, DesktopDeviceInterface, - WebApplicationInterface, WebAppEvent, BackupServiceInterface, DesktopWatchedDirectoriesChanges, } from '@standardnotes/snjs' +import { WebApplicationInterface } from '@standardnotes/ui-services' export class DesktopManager extends ApplicationService @@ -175,7 +175,7 @@ export class DesktopManager return } - const updatedComponent = await this.application.mutator.changeAndSaveItem( + const updatedComponent = await this.application.changeAndSaveItem( component, (m) => { const mutator = m as ComponentMutator diff --git a/packages/web/src/javascripts/Application/Device/WebOrDesktopDevice.ts b/packages/web/src/javascripts/Application/Device/WebOrDesktopDevice.ts index e4cc3b428..dc1699f88 100644 --- a/packages/web/src/javascripts/Application/Device/WebOrDesktopDevice.ts +++ b/packages/web/src/javascripts/Application/Device/WebOrDesktopDevice.ts @@ -108,29 +108,46 @@ export abstract class WebOrDesktopDevice implements WebOrDesktopDeviceInterface identifier: string, ): Promise { const entries = await this.getAllDatabaseEntries(identifier) - const sorted = GetSortedPayloadsByPriority(entries, options) + + const { + itemsKeyPayloads, + keySystemRootKeyPayloads, + keySystemItemsKeyPayloads, + contentTypePriorityPayloads, + remainingPayloads, + } = GetSortedPayloadsByPriority(entries, options) const itemsKeysChunk: DatabaseFullEntryLoadChunk = { - entries: sorted.itemsKeyPayloads, + entries: itemsKeyPayloads, + } + + const keySystemRootKeysChunk: DatabaseFullEntryLoadChunk = { + entries: keySystemRootKeyPayloads, + } + + const keySystemItemsKeysChunk: DatabaseFullEntryLoadChunk = { + entries: keySystemItemsKeyPayloads, } const contentTypePriorityChunk: DatabaseFullEntryLoadChunk = { - entries: sorted.contentTypePriorityPayloads, + entries: contentTypePriorityPayloads, } const remainingPayloadsChunks: DatabaseFullEntryLoadChunk[] = [] - for (let i = 0; i < sorted.remainingPayloads.length; i += options.batchSize) { + for (let i = 0; i < remainingPayloads.length; i += options.batchSize) { remainingPayloadsChunks.push({ - entries: sorted.remainingPayloads.slice(i, i + options.batchSize), + entries: remainingPayloads.slice(i, i + options.batchSize), }) } const result: DatabaseFullEntryLoadChunkResponse = { fullEntries: { itemsKeys: itemsKeysChunk, + keySystemRootKeys: keySystemRootKeysChunk, + keySystemItemsKeys: keySystemItemsKeysChunk, remainingChunks: [contentTypePriorityChunk, ...remainingPayloadsChunks], }, - remainingChunksItemCount: sorted.contentTypePriorityPayloads.length + sorted.remainingPayloads.length, + remainingChunksItemCount: contentTypePriorityPayloads.length + remainingPayloads.length, } return result diff --git a/packages/web/src/javascripts/Application/WebApplication.ts b/packages/web/src/javascripts/Application/WebApplication.ts index 0c3a4a155..6c9548bf8 100644 --- a/packages/web/src/javascripts/Application/WebApplication.ts +++ b/packages/web/src/javascripts/Application/WebApplication.ts @@ -14,7 +14,6 @@ import { ContentType, DecryptedItemInterface, WebAppEvent, - WebApplicationInterface, MobileDeviceInterface, MobileUnlockTiming, DecryptedItem, @@ -27,7 +26,7 @@ import { import { makeObservable, observable } from 'mobx' import { startAuthentication, startRegistration } from '@simplewebauthn/browser' import { PanelResizedData } from '@/Types/PanelResizedData' -import { isAndroid, isDesktopApplication, isIOS } from '@/Utils' +import { isAndroid, isDesktopApplication, isDev, isIOS } from '@/Utils' import { DesktopManager } from './Device/DesktopManager' import { ArchiveManager, @@ -38,7 +37,10 @@ import { RouteService, RouteServiceInterface, ThemeManager, + VaultDisplayService, + VaultDisplayServiceInterface, WebAlertService, + WebApplicationInterface, } from '@standardnotes/ui-services' import { MobileWebReceiver, NativeMobileEventListener } from '../NativeMobileWeb/MobileWebReceiver' import { AndroidBackHandler } from '@/NativeMobileWeb/AndroidBackHandler' @@ -49,6 +51,7 @@ import { FeatureName } from '@/Controllers/FeatureName' import { ItemGroupController } from '@/Components/NoteView/Controller/ItemGroupController' import { VisibilityObserver } from './VisibilityObserver' import { MomentsService } from '@/Controllers/Moments/MomentsService' +import { purchaseMockSubscription } from '@/Utils/Dev/PurchaseMockSubscription' export type WebEventObserver = (event: WebAppEvent, data?: unknown) => void @@ -114,6 +117,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter this.webServices.viewControllerManager.filesController, this.internalEventBus, ) + this.webServices.vaultDisplayService = new VaultDisplayService(this, this.internalEventBus) if (this.isNativeMobileWeb()) { this.mobileWebReceiver = new MobileWebReceiver(this) @@ -194,6 +198,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter this.notifyWebEvent(WebAppEvent.PanelResized, data) } + public get vaultDisplayService(): VaultDisplayServiceInterface { + return this.webServices.vaultDisplayService + } + public getViewControllerManager(): ViewControllerManager { return this.webServices.viewControllerManager } @@ -450,4 +458,12 @@ export class WebApplication extends SNApplication implements WebApplicationInter generateUUID(): string { return this.options.crypto.generateUUID() } + + dev__purchaseMockSubscription() { + if (!isDev) { + throw new Error('This method is only available in dev mode') + } + + void purchaseMockSubscription(this.getUser()?.email as string, 2000) + } } diff --git a/packages/web/src/javascripts/Application/WebServices.ts b/packages/web/src/javascripts/Application/WebServices.ts index fc7ab6a6c..c47c3c0ba 100644 --- a/packages/web/src/javascripts/Application/WebServices.ts +++ b/packages/web/src/javascripts/Application/WebServices.ts @@ -6,6 +6,7 @@ import { ChangelogServiceInterface, KeyboardService, ThemeManager, + VaultDisplayServiceInterface, } from '@standardnotes/ui-services' import { MomentsService } from '@/Controllers/Moments/MomentsService' @@ -18,4 +19,5 @@ export type WebServices = { keyboardService: KeyboardService changelogService: ChangelogServiceInterface momentsService: MomentsService + vaultDisplayService: VaultDisplayServiceInterface } diff --git a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx index 81d74fcc5..765face2f 100644 --- a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx +++ b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx @@ -77,14 +77,14 @@ const ChangeEditorMenu: FunctionComponent = ({ const selectComponent = useCallback( async (component: SNComponent, note: SNNote) => { if (component.conflictOf) { - void application.mutator.changeAndSaveItem(component, (mutator) => { + void application.changeAndSaveItem(component, (mutator) => { mutator.conflictOf = undefined }) } await application.getViewControllerManager().itemListController.insertCurrentIfTemplate() - await application.mutator.changeAndSaveItem(note, (mutator) => { + await application.changeAndSaveItem(note, (mutator) => { const noteMutator = mutator as NoteMutator noteMutator.noteType = component.noteType noteMutator.editorIdentifier = component.identifier @@ -101,7 +101,7 @@ const ChangeEditorMenu: FunctionComponent = ({ reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled)) - await application.mutator.changeAndSaveItem(note, (mutator) => { + await application.changeAndSaveItem(note, (mutator) => { const noteMutator = mutator as NoteMutator noteMutator.noteType = item.noteType noteMutator.editorIdentifier = undefined diff --git a/packages/web/src/javascripts/Components/ChangeEditor/ChangeMultipleMenu.tsx b/packages/web/src/javascripts/Components/ChangeEditor/ChangeMultipleMenu.tsx index 903631dd0..8e76f5ada 100644 --- a/packages/web/src/javascripts/Components/ChangeEditor/ChangeMultipleMenu.tsx +++ b/packages/web/src/javascripts/Components/ChangeEditor/ChangeMultipleMenu.tsx @@ -42,12 +42,12 @@ const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Prop const selectComponent = useCallback( async (component: SNComponent, note: SNNote) => { if (component.conflictOf) { - void application.mutator.changeAndSaveItem(component, (mutator) => { + void application.changeAndSaveItem(component, (mutator) => { mutator.conflictOf = undefined }) } - await application.mutator.changeAndSaveItem(note, (mutator) => { + await application.changeAndSaveItem(note, (mutator) => { const noteMutator = mutator as NoteMutator noteMutator.noteType = component.noteType noteMutator.editorIdentifier = component.identifier @@ -58,7 +58,7 @@ const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Prop const selectNonComponent = useCallback( async (item: EditorMenuItem, note: SNNote) => { - await application.mutator.changeAndSaveItem(note, (mutator) => { + await application.changeAndSaveItem(note, (mutator) => { const noteMutator = mutator as NoteMutator noteMutator.noteType = item.noteType noteMutator.editorIdentifier = undefined diff --git a/packages/web/src/javascripts/Components/ClipperView/ClippedNoteView.tsx b/packages/web/src/javascripts/Components/ClipperView/ClippedNoteView.tsx index 3236652fb..899859aba 100644 --- a/packages/web/src/javascripts/Components/ClipperView/ClippedNoteView.tsx +++ b/packages/web/src/javascripts/Components/ClipperView/ClippedNoteView.tsx @@ -64,6 +64,7 @@ const ClippedNoteView = ({ setIsDiscarding(true) application.mutator .deleteItem(note) + .then(() => application.sync.sync()) .then(() => { if (isFirefoxPopup) { window.close() @@ -73,7 +74,7 @@ const ClippedNoteView = ({ .catch(console.error) .finally(() => setIsDiscarding(false)) } - }, [application.mutator, clearClip, isFirefoxPopup, note]) + }, [application.mutator, application.sync, clearClip, isFirefoxPopup, note]) return (
diff --git a/packages/web/src/javascripts/Components/ClipperView/ClipperView.tsx b/packages/web/src/javascripts/Components/ClipperView/ClipperView.tsx index 2e9fdc7cb..6eac72f01 100644 --- a/packages/web/src/javascripts/Components/ClipperView/ClipperView.tsx +++ b/packages/web/src/javascripts/Components/ClipperView/ClipperView.tsx @@ -217,7 +217,7 @@ const ClipperView = ({ references: [], }) - const insertedNote = await application.items.insertItem(note) + const insertedNote = await application.mutator.insertItem(note) if (defaultTagRef.current) { await application.linkingController.linkItems(insertedNote, defaultTagRef.current) @@ -237,6 +237,7 @@ const ClipperView = ({ }, [ application.items, application.linkingController, + application.mutator, application.sync, clipPayload, defaultTagRef, diff --git a/packages/web/src/javascripts/Components/ContentListView/FileListItem.tsx b/packages/web/src/javascripts/Components/ContentListView/FileListItem.tsx index 43bc68d7e..97fe843a6 100644 --- a/packages/web/src/javascripts/Components/ContentListView/FileListItem.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/FileListItem.tsx @@ -13,6 +13,7 @@ import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType' import { useApplication } from '../ApplicationProvider' import { PaneLayout } from '@/Controllers/PaneController/PaneLayout' import ListItemFlagIcons from './ListItemFlagIcons' +import ListItemVaultInfo from './ListItemVaultInfo' const FileListItemCard: FunctionComponent> = ({ filesController, @@ -103,6 +104,7 @@ const FileListItemCard: FunctionComponent> = +
diff --git a/packages/web/src/javascripts/Components/ContentListView/FileListItemCard.tsx b/packages/web/src/javascripts/Components/ContentListView/FileListItemCard.tsx index c80bdcc0a..285730575 100644 --- a/packages/web/src/javascripts/Components/ContentListView/FileListItemCard.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/FileListItemCard.tsx @@ -14,6 +14,7 @@ import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType' import { useApplication } from '../ApplicationProvider' import Icon from '../Icon/Icon' import { PaneLayout } from '@/Controllers/PaneController/PaneLayout' +import ListItemVaultInfo from './ListItemVaultInfo' const FileListItemCard: FunctionComponent> = ({ filesController, @@ -106,6 +107,7 @@ const FileListItemCard: FunctionComponent> = +
)} -
+
{panelTitle}
{showSyncSubtitle &&
{syncSubtitle}
} {optionsSubtitle &&
{optionsSubtitle}
}
+
) - }, [optionsSubtitle, showSyncSubtitle, icon, panelTitle, syncSubtitle]) + }, [optionsSubtitle, selectedTag, showSyncSubtitle, icon, panelTitle, syncSubtitle]) const PhoneAndDesktopLayout = useMemo(() => { return ( diff --git a/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx b/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx index 827f75e4c..83c2ccb25 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/Header/DisplayOptionsMenu.tsx @@ -181,7 +181,7 @@ const DisplayOptionsMenu: FunctionComponent = ({ } else if (isSystemTag) { await changeSystemViewPreferences(properties) } else { - await application.mutator.changeAndSaveItem(selectedTag, (mutator) => { + await application.changeAndSaveItem(selectedTag, (mutator) => { mutator.preferences = { ...mutator.preferences, ...properties, @@ -189,7 +189,7 @@ const DisplayOptionsMenu: FunctionComponent = ({ }) } }, - [currentMode, isSystemTag, changeGlobalPreferences, changeSystemViewPreferences, application.mutator, selectedTag], + [currentMode, isSystemTag, changeGlobalPreferences, changeSystemViewPreferences, application, selectedTag], ) const resetTagPreferences = useCallback(async () => { @@ -202,7 +202,7 @@ const DisplayOptionsMenu: FunctionComponent = ({ return } - void application.mutator.changeAndSaveItem(selectedTag, (mutator) => { + void application.changeAndSaveItem(selectedTag, (mutator) => { mutator.preferences = undefined }) }, [application, isSystemTag, reloadPreferences, selectedTag]) diff --git a/packages/web/src/javascripts/Components/ContentListView/ListItemVaultInfo.tsx b/packages/web/src/javascripts/Components/ContentListView/ListItemVaultInfo.tsx new file mode 100644 index 000000000..e3dc54768 --- /dev/null +++ b/packages/web/src/javascripts/Components/ContentListView/ListItemVaultInfo.tsx @@ -0,0 +1,46 @@ +import { FunctionComponent } from 'react' +import { useApplication } from '../ApplicationProvider' +import Icon from '../Icon/Icon' +import { DecryptedItemInterface } from '@standardnotes/snjs' +import VaultNameBadge from '../Vaults/VaultNameBadge' +import { featureTrunkVaultsEnabled } from '@/FeatureTrunk' + +type Props = { + item: DecryptedItemInterface +} + +const ListItemVaultInfo: FunctionComponent = ({ item }) => { + const application = useApplication() + + if (!featureTrunkVaultsEnabled()) { + return null + } + + if (application.items.isTemplateItem(item)) { + return null + } + + const vault = application.vaults.getItemVault(item) + if (!vault) { + return null + } + + const sharedByContact = application.sharedVaults.getItemSharedBy(item) + + return ( +
+ + + {sharedByContact && ( +
+ + +
{sharedByContact?.name}
+
+
+ )} +
+ ) +} + +export default ListItemVaultInfo diff --git a/packages/web/src/javascripts/Components/ContentListView/NoteListItem.tsx b/packages/web/src/javascripts/Components/ContentListView/NoteListItem.tsx index b0a031f56..bfd46b48a 100644 --- a/packages/web/src/javascripts/Components/ContentListView/NoteListItem.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/NoteListItem.tsx @@ -13,6 +13,7 @@ import { ListItemTitle } from './ListItemTitle' import { log, LoggingDomain } from '@/Logging' import { classNames } from '@standardnotes/utils' import { getIconAndTintForNoteType } from '@/Utils/Items/Icons/getIconAndTintForNoteType' +import ListItemVaultInfo from './ListItemVaultInfo' import { NoteDragDataFormat } from '../Tags/DragNDrop' import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' @@ -143,6 +144,7 @@ const NoteListItem: FunctionComponent> = ({ + diff --git a/packages/web/src/javascripts/Components/ContentTableView/ContentTableView.tsx b/packages/web/src/javascripts/Components/ContentTableView/ContentTableView.tsx index b764021e3..ed079da1d 100644 --- a/packages/web/src/javascripts/Components/ContentTableView/ContentTableView.tsx +++ b/packages/web/src/javascripts/Components/ContentTableView/ContentTableView.tsx @@ -39,6 +39,7 @@ import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalCo import { useItemLinks } from '@/Hooks/useItemLinks' import { ItemLink } from '@/Utils/Items/Search/ItemLink' import { ItemListController } from '@/Controllers/ItemList/ItemListController' +import ListItemVaultInfo from '../ContentListView/ListItemVaultInfo' const ContextMenuCell = ({ items, @@ -213,6 +214,7 @@ const ItemNameCell = ({ item, hideIcon }: { item: DecryptedItemInterface; hideIc )} {item.title} + {item.protected && ( @@ -245,7 +247,7 @@ const AttachedToCell = ({ item }: { item: DecryptedItemInterface }) => { link={allLinks[0]} key={allLinks[0].id} unlinkItem={async (itemToUnlink) => { - void application.items.unlinkItems(item, itemToUnlink) + void application.mutator.unlinkItems(item, itemToUnlink) }} isBidirectional={false} /> @@ -312,7 +314,7 @@ const ContentTableView = ({ return } - await application.mutator.changeAndSaveItem(selectedTag, (mutator) => { + await application.changeAndSaveItem(selectedTag, (mutator) => { mutator.preferences = { ...mutator.preferences, sortBy, diff --git a/packages/web/src/javascripts/Components/FileContextMenu/FileMenuOptions.tsx b/packages/web/src/javascripts/Components/FileContextMenu/FileMenuOptions.tsx index 99dc34a96..4c69277c9 100644 --- a/packages/web/src/javascripts/Components/FileContextMenu/FileMenuOptions.tsx +++ b/packages/web/src/javascripts/Components/FileContextMenu/FileMenuOptions.tsx @@ -15,6 +15,9 @@ import AddTagOption from '../NotesOptions/AddTagOption' import { MenuItemIconSize } from '@/Constants/TailwindClassNames' import { LinkingController } from '@/Controllers/LinkingController' import { NavigationController } from '@/Controllers/Navigation/NavigationController' +import AddToVaultMenuOption from '../Vaults/AddToVaultMenuOption' +import { iconClass } from '../NotesOptions/ClassNames' +import { featureTrunkVaultsEnabled } from '@/FeatureTrunk' type Props = { closeMenu: () => void @@ -90,6 +93,7 @@ const FileMenuOptions: FunctionComponent = ({ ) : null} )} + {featureTrunkVaultsEnabled() && } { - await application.items.renameFile(file, event.target.value) + await application.mutator.renameFile(file, event.target.value) void application.sync.sync() }, syncDebounceMs) }, diff --git a/packages/web/src/javascripts/Components/Footer/Footer.tsx b/packages/web/src/javascripts/Components/Footer/Footer.tsx index 15bb34f81..883d400c2 100644 --- a/packages/web/src/javascripts/Components/Footer/Footer.tsx +++ b/packages/web/src/javascripts/Components/Footer/Footer.tsx @@ -21,6 +21,7 @@ import AccountMenuButton from './AccountMenuButton' import StyledTooltip from '../StyledTooltip/StyledTooltip' import UpgradeNow from './UpgradeNow' import PreferencesButton from './PreferencesButton' +import VaultSelectionButton from './VaultSelectionButton' type Props = { application: WebApplication @@ -37,6 +38,7 @@ type State = { newUpdateAvailable: boolean showAccountMenu: boolean showQuickSettingsMenu: boolean + showVaultSelectionMenu: boolean offline: boolean hasError: boolean arbitraryStatusMessage?: string @@ -64,6 +66,7 @@ class Footer extends AbstractComponent { newUpdateAvailable: false, showAccountMenu: false, showQuickSettingsMenu: false, + showVaultSelectionMenu: false, } this.webEventListenerDestroyer = props.application.addWebEventObserver((event, data) => { @@ -125,6 +128,7 @@ class Footer extends AbstractComponent { showBetaWarning: showBetaWarning, showAccountMenu: this.viewControllerManager.accountMenuController.show, showQuickSettingsMenu: this.viewControllerManager.quickSettingsMenuController.open, + showVaultSelectionMenu: this.viewControllerManager.vaultSelectionController.open, }) }) } @@ -296,6 +300,10 @@ class Footer extends AbstractComponent { this.viewControllerManager.quickSettingsMenuController.toggle() } + vaultSelectionClickHandler = () => { + this.viewControllerManager.vaultSelectionController.toggle() + } + syncResolutionClickHandler = () => { this.setState({ showSyncResolution: !this.state.showSyncResolution, @@ -367,9 +375,11 @@ class Footer extends AbstractComponent { viewControllerManager={this.viewControllerManager} /> +
+
{ quickSettingsMenuController={this.viewControllerManager.quickSettingsMenuController} />
+ +
+ +
theme.package_info.identifier === FeatureIdentifier.DarkTheme) as SNTheme | undefined if (darkTheme) { - void application.mutator.toggleTheme(darkTheme) + void application.componentManager.toggleTheme(darkTheme.uuid) } }, }) diff --git a/packages/web/src/javascripts/Components/Footer/VaultSelectionButton.tsx b/packages/web/src/javascripts/Components/Footer/VaultSelectionButton.tsx new file mode 100644 index 000000000..ceebc9fe1 --- /dev/null +++ b/packages/web/src/javascripts/Components/Footer/VaultSelectionButton.tsx @@ -0,0 +1,62 @@ +import { classNames } from '@standardnotes/utils' +import { useRef } from 'react' +import Icon from '../Icon/Icon' +import Popover from '../Popover/Popover' +import StyledTooltip from '../StyledTooltip/StyledTooltip' +import { VaultSelectionMenuController } from '@/Controllers/VaultSelectionMenuController' +import VaultSelectionMenu from '../VaultSelectionMenu/VaultSelectionMenu' +import { useApplication } from '../ApplicationProvider' +import { observer } from 'mobx-react-lite' +import { featureTrunkVaultsEnabled } from '@/FeatureTrunk' + +type Props = { + isOpen: boolean + toggleMenu: () => void + controller: VaultSelectionMenuController +} + +const VaultSelectionButton = ({ isOpen, toggleMenu, controller }: Props) => { + const application = useApplication() + const buttonRef = useRef(null) + const exclusivelyShownVault = application.vaultDisplayService.exclusivelyShownVault + + if (!featureTrunkVaultsEnabled()) { + return null + } + + return ( + <> + + + + + + + + ) +} + +export default observer(VaultSelectionButton) diff --git a/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx b/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx index 2cb9a0356..32a8483c6 100644 --- a/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx +++ b/packages/web/src/javascripts/Components/Icon/IconNameToSvgMapping.tsx @@ -80,6 +80,7 @@ export const IconNameToSvgMapping = { check: icons.CheckIcon, close: icons.CloseIcon, code: icons.CodeIcon, + comment: icons.FeedbackIcon, copy: icons.CopyIcon, dashboard: icons.DashboardIcon, diamond: icons.DiamondIcon, @@ -92,6 +93,7 @@ export const IconNameToSvgMapping = { file: icons.FileIcon, folder: icons.FolderIcon, gkeep: icons.GoogleKeepIcon, + group: icons.GroupIcon, hashtag: icons.HashtagIcon, help: icons.HelpIcon, history: icons.HistoryIcon, diff --git a/packages/web/src/javascripts/Components/NoteView/CollaborationInfoHUD.tsx b/packages/web/src/javascripts/Components/NoteView/CollaborationInfoHUD.tsx new file mode 100644 index 000000000..5376f0162 --- /dev/null +++ b/packages/web/src/javascripts/Components/NoteView/CollaborationInfoHUD.tsx @@ -0,0 +1,46 @@ +import { FunctionComponent } from 'react' +import Icon from '../Icon/Icon' +import { useApplication } from '../ApplicationProvider' +import { DecryptedItemInterface } from '@standardnotes/snjs' +import { featureTrunkVaultsEnabled } from '@/FeatureTrunk' + +type Props = { + item: DecryptedItemInterface +} + +const CollaborationInfoHUD: FunctionComponent = ({ item }) => { + const application = useApplication() + + if (!featureTrunkVaultsEnabled()) { + return null + } + + if (application.items.isTemplateItem(item)) { + return null + } + + const vault = application.vaults.getItemVault(item) + if (!vault) { + return null + } + + const lastEditedBy = application.sharedVaults.getItemLastEditedBy(item) + + return ( +
+
+ + {vault.name} +
+ + {lastEditedBy && ( +
+ + {lastEditedBy?.name} +
+ )} +
+ ) +} + +export default CollaborationInfoHUD diff --git a/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts index e481028e2..64cefb691 100644 --- a/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts +++ b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.spec.ts @@ -5,10 +5,11 @@ import { SNComponentManager, SNComponent, SNTag, - ItemsClientInterface, SNNote, Deferred, SyncServiceInterface, + ItemManagerInterface, + MutatorClientInterface, } from '@standardnotes/snjs' import { FeatureIdentifier, NoteType } from '@standardnotes/features' import { NoteViewController } from './NoteViewController' @@ -24,7 +25,9 @@ describe('note view controller', () => { application.noAccount = jest.fn().mockReturnValue(false) application.isNativeMobileWeb = jest.fn().mockReturnValue(false) - Object.defineProperty(application, 'items', { value: {} as jest.Mocked }) + const items = {} as jest.Mocked + items.createTemplateItem = jest.fn().mockReturnValue({} as SNNote) + Object.defineProperty(application, 'items', { value: items }) Object.defineProperty(application, 'sync', { value: {} as jest.Mocked }) application.sync.sync = jest.fn().mockReturnValue(Promise.resolve()) @@ -33,8 +36,7 @@ describe('note view controller', () => { componentManager.legacyGetDefaultEditor = jest.fn() Object.defineProperty(application, 'componentManager', { value: componentManager }) - const mutator = {} as jest.Mocked - mutator.createTemplateItem = jest.fn().mockReturnValue({} as SNNote) + const mutator = {} as jest.Mocked Object.defineProperty(application, 'mutator', { value: mutator }) }) @@ -44,7 +46,7 @@ describe('note view controller', () => { const controller = new NoteViewController(application) await controller.initialize() - expect(application.mutator.createTemplateItem).toHaveBeenCalledWith( + expect(application.items.createTemplateItem).toHaveBeenCalledWith( ContentType.Note, expect.objectContaining({ noteType: NoteType.Plain }), expect.anything(), @@ -65,7 +67,7 @@ describe('note view controller', () => { const controller = new NoteViewController(application) await controller.initialize() - expect(application.mutator.createTemplateItem).toHaveBeenCalledWith( + expect(application.items.createTemplateItem).toHaveBeenCalledWith( ContentType.Note, expect.objectContaining({ noteType: NoteType.Markdown }), expect.anything(), @@ -80,13 +82,13 @@ describe('note view controller', () => { } as jest.Mocked application.items.findItem = jest.fn().mockReturnValue(tag) - application.items.addTagToNote = jest.fn() + application.mutator.addTagToNote = jest.fn() const controller = new NoteViewController(application, undefined, { tag: tag.uuid }) await controller.initialize() expect(controller['defaultTag']).toEqual(tag) - expect(application.items.addTagToNote).toHaveBeenCalledWith(expect.anything(), tag, expect.anything()) + expect(application.mutator.addTagToNote).toHaveBeenCalledWith(expect.anything(), tag, expect.anything()) }) it('should wait until item finishes saving locally before deiniting', async () => { diff --git a/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.ts b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.ts index 714a8d8be..1438ea313 100644 --- a/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.ts +++ b/packages/web/src/javascripts/Components/NoteView/Controller/NoteViewController.ts @@ -1,6 +1,14 @@ import { WebApplication } from '@/Application/WebApplication' import { noteTypeForEditorIdentifier } from '@standardnotes/features' -import { SNNote, SNTag, NoteContent, DecryptedItemInterface, PayloadEmitSource, PrefKey } from '@standardnotes/models' +import { + SNNote, + SNTag, + NoteContent, + DecryptedItemInterface, + PayloadEmitSource, + PrefKey, + PayloadVaultOverrides, +} from '@standardnotes/models' import { UuidString } from '@standardnotes/snjs' import { removeFromArray } from '@standardnotes/utils' import { ContentType } from '@standardnotes/common' @@ -90,7 +98,7 @@ export class NoteViewController implements ItemViewControllerInterface { const noteType = noteTypeForEditorIdentifier(editorIdentifier) - const note = this.application.mutator.createTemplateItem( + const note = this.application.items.createTemplateItem( ContentType.Note, { text: '', @@ -101,6 +109,7 @@ export class NoteViewController implements ItemViewControllerInterface { }, { created_at: this.templateNoteOptions?.createdAt || new Date(), + ...PayloadVaultOverrides(this.templateNoteOptions?.vault), }, ) @@ -110,7 +119,7 @@ export class NoteViewController implements ItemViewControllerInterface { if (this.defaultTagUuid) { const tag = this.application.items.findItem(this.defaultTagUuid) as SNTag - await this.application.items.addTagToNote(note, tag, addTagHierarchy) + await this.application.mutator.addTagToNote(note, tag, addTagHierarchy) } this.notifyObservers(this.item, PayloadEmitSource.InitialObserverRegistrationPush) diff --git a/packages/web/src/javascripts/Components/NoteView/Controller/TemplateNoteViewControllerOptions.ts b/packages/web/src/javascripts/Components/NoteView/Controller/TemplateNoteViewControllerOptions.ts index 898dbe32f..513d981ff 100644 --- a/packages/web/src/javascripts/Components/NoteView/Controller/TemplateNoteViewControllerOptions.ts +++ b/packages/web/src/javascripts/Components/NoteView/Controller/TemplateNoteViewControllerOptions.ts @@ -1,8 +1,9 @@ -import { UuidString } from '@standardnotes/snjs' +import { UuidString, VaultListingInterface } from '@standardnotes/snjs' export type TemplateNoteViewControllerOptions = { title?: string tag?: UuidString + vault?: VaultListingInterface createdAt?: Date autofocusBehavior?: TemplateNoteViewAutofocusBehavior } diff --git a/packages/web/src/javascripts/Components/NoteView/NoteConflictResolutionModal/NoteConflictResolutionModal.tsx b/packages/web/src/javascripts/Components/NoteView/NoteConflictResolutionModal/NoteConflictResolutionModal.tsx index ad5c0fa4a..a2a31818d 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteConflictResolutionModal/NoteConflictResolutionModal.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteConflictResolutionModal/NoteConflictResolutionModal.tsx @@ -65,12 +65,13 @@ const NoteConflictResolutionModal = ({ async (note: SNNote) => { await application.mutator .deleteItem(note) + .then(() => application.sync.sync()) .catch(console.error) .then(() => { setSelectedVersions([allVersions[0].uuid]) }) }, - [allVersions, application.mutator], + [allVersions, application.mutator, application.sync], ) const [selectedAction, setSelectionAction] = useState('move-to-trash') diff --git a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx index 8ade849a5..38f2fc2c2 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx @@ -45,6 +45,7 @@ import { SuperEditorContentId } from '../SuperEditor/Constants' import { NoteViewController } from './Controller/NoteViewController' import { PlainEditor, PlainEditorInterface } from './PlainEditor/PlainEditor' import { EditorMargins, EditorMaxWidths } from '../EditorWidthSelectionModal/EditorWidths' +import CollaborationInfoHUD from './CollaborationInfoHUD' import Button from '../Button/Button' import ModalOverlay from '../Modal/ModalOverlay' import NoteConflictResolutionModal from './NoteConflictResolutionModal/NoteConflictResolutionModal' @@ -74,7 +75,7 @@ type State = { monospaceFont?: boolean plainEditorFocused?: boolean paneGestureEnabled?: boolean - + noteLastEditedByUuid?: string updateSavingIndicator?: boolean editorFeatureIdentifier?: string noteType?: NoteType @@ -270,6 +271,12 @@ class NoteView extends AbstractComponent { }) } + if (note.last_edited_by_uuid !== this.state.noteLastEditedByUuid) { + this.setState({ + noteLastEditedByUuid: note.last_edited_by_uuid, + }) + } + if (note.locked !== this.state.noteLocked) { this.setState({ noteLocked: note.locked, @@ -651,7 +658,10 @@ class NoteView extends AbstractComponent { } performNoteDeletion(note: SNNote) { - this.application.mutator.deleteItem(note).catch(console.error) + this.application.mutator + .deleteItem(note) + .then(() => this.application.sync.sync()) + .catch(console.error) } onPanelResizeFinish = async (width: number, left: number, isMaxWidth: boolean) => { @@ -897,6 +907,7 @@ class NoteView extends AbstractComponent { )} {renderHeaderOptions && (
+ { await linkingController.linkItems(note, uploadedFile) - void application.mutator.changeAndSaveItem(uploadedFile, (mutator) => { + void application.changeAndSaveItem(uploadedFile, (mutator) => { mutator.protected = note.protected }) filesController.notifyObserversOfUploadedFileLinkingToCurrentNote(uploadedFile.uuid) @@ -37,7 +37,7 @@ const NoteViewFileDropTarget = ({ note, linkingController, noteViewElement, file removeDragTarget(target) } } - }, [addDragTarget, linkingController, note, noteViewElement, removeDragTarget, filesController, application.mutator]) + }, [addDragTarget, linkingController, note, noteViewElement, removeDragTarget, filesController, application]) return isDraggingFiles ? ( // Required to block drag events to editor iframe diff --git a/packages/web/src/javascripts/Components/NoteView/ReadonlyNoteContent.tsx b/packages/web/src/javascripts/Components/NoteView/ReadonlyNoteContent.tsx index cbc788fda..b4e1d1261 100644 --- a/packages/web/src/javascripts/Components/NoteView/ReadonlyNoteContent.tsx +++ b/packages/web/src/javascripts/Components/NoteView/ReadonlyNoteContent.tsx @@ -36,14 +36,14 @@ export const ReadonlyNoteContent = ({ return undefined } - const templateNoteForRevision = application.mutator.createTemplateItem(ContentType.Note, note.content) as SNNote + const templateNoteForRevision = application.items.createTemplateItem(ContentType.Note, note.content) as SNNote const componentViewer = application.componentManager.createComponentViewer(editorForCurrentNote) componentViewer.setReadonly(true) componentViewer.lockReadonly = true componentViewer.overrideContextItem = templateNoteForRevision return componentViewer - }, [application.componentManager, application.mutator, note]) + }, [application.componentManager, application.items, note]) useEffect(() => { return () => { diff --git a/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx b/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx index c0d030ea9..1d0bfe686 100644 --- a/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx +++ b/packages/web/src/javascripts/Components/NotesOptions/NotesOptions.tsx @@ -37,6 +37,8 @@ import ModalOverlay from '../Modal/ModalOverlay' import SuperExportModal from './SuperExportModal' import { useApplication } from '../ApplicationProvider' import { MutuallyExclusiveMediaQueryBreakpoints } from '@/Hooks/useMediaQuery' +import AddToVaultMenuOption from '../Vaults/AddToVaultMenuOption' +import { featureTrunkVaultsEnabled } from '@/FeatureTrunk' const iconSize = MenuItemIconSize const iconClassDanger = `text-danger mr-2 ${iconSize}` @@ -144,8 +146,9 @@ const NotesOptions = ({ const duplicateSelectedItems = useCallback(async () => { await Promise.all(notes.map((note) => application.mutator.duplicateItem(note).catch(console.error))) + void application.sync.sync() closeMenuAndToggleNotesList() - }, [application.mutator, closeMenuAndToggleNotesList, notes]) + }, [application.mutator, application.sync, closeMenuAndToggleNotesList, notes]) const openRevisionHistoryModal = useCallback(() => { historyModalController.openModal(notesController.firstSelectedNote) @@ -240,6 +243,9 @@ const NotesOptions = ({ )} + + {featureTrunkVaultsEnabled() && } + {navigationController.tagsCount > 0 && ( = ({ menu, @@ -40,6 +41,8 @@ const PaneSelector: FunctionComponent ) + case 'vaults': + return case 'backups': return case 'listed': diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Backups/DataBackups.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Backups/DataBackups.tsx index b9309739a..806825a11 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Backups/DataBackups.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Backups/DataBackups.tsx @@ -109,7 +109,7 @@ const DataBackups = ({ application, viewControllerManager }: Props) => { const performImport = async (data: BackupFile) => { setIsImportDataLoading(true) - const result = await application.mutator.importData(data) + const result = await application.importData(data) setIsImportDataLoading(false) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/Packages/PackageEntry.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/Packages/PackageEntry.tsx index 777b9c0a1..97bb09d65 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/Packages/PackageEntry.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/Packages/PackageEntry.tsx @@ -34,7 +34,7 @@ const PackageEntry: FunctionComponent = ({ application, exten const toggleOfflineOnly = () => { const newOfflineOnly = !offlineOnly setOfflineOnly(newOfflineOnly) - application.mutator + application .changeAndSaveItem(extension, (mutator) => { mutator.offlineOnly = newOfflineOnly }) @@ -49,7 +49,7 @@ const PackageEntry: FunctionComponent = ({ application, exten const changeExtensionName = (newName: string) => { setExtensionName(newName) - application.mutator + application .changeAndSaveItem(extension, (mutator) => { mutator.name = newName }) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/Packages/Section.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/Packages/Section.tsx index 3321a7749..939543682 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/Packages/Section.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/Packages/Section.tsx @@ -53,6 +53,7 @@ const PackagesPreferencesSection: FunctionComponent = ({ .then(async (shouldRemove: boolean) => { if (shouldRemove) { await application.mutator.deleteItem(extension) + void application.sync.sync() setExtensions(loadExtensions(application)) } }) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/Persistence.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/General/Persistence.tsx index 3e46796f9..545b6006a 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/Persistence.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/Persistence.tsx @@ -12,7 +12,9 @@ type Props = { export const ShouldPersistNoteStateKey = 'ShouldPersistNoteState' const Persistence = ({ application }: Props) => { - const [shouldPersistNoteState, setShouldPersistNoteState] = useState(application.getValue(ShouldPersistNoteStateKey)) + const [shouldPersistNoteState, setShouldPersistNoteState] = useState( + application.getValue(ShouldPersistNoteStateKey), + ) const toggleStatePersistence = (shouldPersist: boolean) => { application.setValue(ShouldPersistNoteStateKey, shouldPersist) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/SmartViews/EditSmartViewModalController.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/General/SmartViews/EditSmartViewModalController.tsx index 1f01da0b5..1b075bfc5 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/SmartViews/EditSmartViewModalController.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/SmartViews/EditSmartViewModalController.tsx @@ -88,7 +88,7 @@ export class EditSmartViewModalController { this.setIsSaving(true) - await this.application.mutator.changeAndSaveItem(this.view, (mutator) => { + await this.application.changeAndSaveItem(this.view, (mutator) => { mutator.title = this.title mutator.iconString = (this.icon as string) || SmartViewDefaultIconName mutator.predicate = JSON.parse(this.predicateJson) as PredicateJsonForm @@ -111,7 +111,10 @@ export class EditSmartViewModalController { confirmButtonStyle: 'danger', }) if (shouldDelete) { - this.application.mutator.deleteItem(view).catch(console.error) + this.application.mutator + .deleteItem(view) + .then(() => this.application.sync.sync()) + .catch(console.error) } } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/SmartViews/SmartViews.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/General/SmartViews/SmartViews.tsx index 6a5b80f5a..47dd3859f 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/SmartViews/SmartViews.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/SmartViews/SmartViews.tsx @@ -47,10 +47,13 @@ const SmartViews = ({ application, featuresController }: Props) => { confirmButtonStyle: 'danger', }) if (shouldDelete) { - application.mutator.deleteItem(view).catch(console.error) + application.mutator + .deleteItem(view) + .then(() => application.sync.sync()) + .catch(console.error) } }, - [application.mutator], + [application.mutator, application.sync], ) return ( diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/EncryptionEnabled.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Security/EncryptionEnabled.tsx index 0814199b6..35ae3d433 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Security/EncryptionEnabled.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/EncryptionEnabled.tsx @@ -1,6 +1,6 @@ import { useApplication } from '@/Components/ApplicationProvider' import Icon from '@/Components/Icon/Icon' -import { ContentType, ItemCounter } from '@standardnotes/snjs' +import { ContentType, StaticItemCounter } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent } from 'react' import EncryptionStatusItem from './EncryptionStatusItem' @@ -8,7 +8,7 @@ import { formatCount } from './formatCount' const EncryptionEnabled: FunctionComponent = () => { const application = useApplication() - const itemCounter = new ItemCounter() + const itemCounter = new StaticItemCounter() const count = itemCounter.countNotesAndTags(application.items.getItems([ContentType.Note, ContentType.Tag])) const files = application.items.getItems([ContentType.File]) const notes = formatCount(count.notes, 'notes') diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/ErroredItems.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Security/ErroredItems.tsx index 2170caf29..8fddeed72 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Security/ErroredItems.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/ErroredItems.tsx @@ -18,7 +18,7 @@ type Props = { viewControllerManager: ViewControllerManager } const ErroredItems: FunctionComponent = ({ viewControllerManager }: Props) => { const app = viewControllerManager.application - const [erroredItems, setErroredItems] = useState(app.items.invalidItems) + const [erroredItems, setErroredItems] = useState(app.items.invalidNonVaultedItems) const getContentTypeDisplay = (item: EncryptedItemInterface): string => { const display = DisplayStringForContentType(item.content_type) @@ -44,7 +44,9 @@ const ErroredItems: FunctionComponent = ({ viewControllerManager }: Props return } - void app.mutator.deleteItems(items) + void app.mutator.deleteItems(items).then(() => { + void app.sync.sync() + }) setErroredItems(app.items.invalidItems) } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/Security.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Security/Security.tsx index 765d52559..a2033d0ec 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Security/Security.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/Security.tsx @@ -30,7 +30,7 @@ const Security: FunctionComponent = (props) => { return ( - {props.application.items.invalidItems.length > 0 && ( + {props.application.items.invalidNonVaultedItems.length > 0 && ( )} diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Security/securityPrefsHasBubble.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Security/securityPrefsHasBubble.tsx index 00d491368..8d3adcf27 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Security/securityPrefsHasBubble.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Security/securityPrefsHasBubble.tsx @@ -1,5 +1,5 @@ import { WebApplication } from '@/Application/WebApplication' export const securityPrefsHasBubble = (application: WebApplication): boolean => { - return application.items.invalidItems.length > 0 + return application.items.invalidNonVaultedItems.length > 0 } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Contacts/ContactItem.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Contacts/ContactItem.tsx new file mode 100644 index 000000000..948c0fc86 --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Contacts/ContactItem.tsx @@ -0,0 +1,51 @@ +import Button from '@/Components/Button/Button' +import Icon from '@/Components/Icon/Icon' +import ModalOverlay from '@/Components/Modal/ModalOverlay' +import { TrustedContactInterface } from '@standardnotes/snjs' +import EditContactModal from './EditContactModal' +import { useCallback, useState } from 'react' +import { useApplication } from '@/Components/ApplicationProvider' + +type Props = { + contact: TrustedContactInterface +} + +const ContactItem = ({ contact }: Props) => { + const application = useApplication() + + const [isContactModalOpen, setIsContactModalOpen] = useState(false) + const closeContactModal = () => setIsContactModalOpen(false) + + const collaborationID = application.contacts.getCollaborationIDForTrustedContact(contact) + + const deleteContact = useCallback(async () => { + void application.contacts.deleteContact(contact) + }, [application.contacts, contact]) + + return ( + <> + + + + +
+ +
+ + {contact.name} + + {collaborationID} + +
+
+
+
+ + ) +} + +export default ContactItem diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Contacts/EditContactModal.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Contacts/EditContactModal.tsx new file mode 100644 index 000000000..9062c77d1 --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Contacts/EditContactModal.tsx @@ -0,0 +1,127 @@ +import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react' +import Modal, { ModalAction } from '@/Components/Modal/Modal' +import DecoratedInput from '@/Components/Input/DecoratedInput' +import { useApplication } from '@/Components/ApplicationProvider' +import { PendingSharedVaultInviteRecord, TrustedContactInterface } from '@standardnotes/snjs' + +type Props = { + fromInvite?: PendingSharedVaultInviteRecord + editContactUuid?: string + onCloseDialog: () => void + onAddContact?: (contact: TrustedContactInterface) => void +} + +const EditContactModal: FunctionComponent = ({ onCloseDialog, fromInvite, onAddContact, editContactUuid }) => { + const application = useApplication() + + const [name, setName] = useState('') + const [collaborationID, setCollaborationID] = useState('') + const [editingContact, setEditingContact] = useState(undefined) + + const handleDialogClose = useCallback(() => { + onCloseDialog() + }, [onCloseDialog]) + + useEffect(() => { + if (fromInvite) { + setCollaborationID(application.contacts.getCollaborationIDFromInvite(fromInvite.invite)) + } + }, [application.contacts, fromInvite]) + + useEffect(() => { + if (editContactUuid) { + const contact = application.contacts.findTrustedContact(editContactUuid) + if (!contact) { + throw new Error(`Contact with uuid ${editContactUuid} not found`) + } + + setEditingContact(contact) + setName(contact.name) + setCollaborationID(application.contacts.getCollaborationIDForTrustedContact(contact)) + } + }, [application.contacts, application.vaults, editContactUuid]) + + const handleSubmit = useCallback(async () => { + if (editingContact) { + void application.contacts.editTrustedContactFromCollaborationID(editingContact, { name, collaborationID }) + handleDialogClose() + } else { + const contact = await application.contacts.addTrustedContactFromCollaborationID(collaborationID, name) + if (contact) { + onAddContact?.(contact) + handleDialogClose() + } else { + void application.alertService.alert('Unable to create contact. Please try again.') + } + } + }, [ + editingContact, + application.contacts, + application.alertService, + name, + collaborationID, + handleDialogClose, + onAddContact, + ]) + + const modalActions = useMemo( + (): ModalAction[] => [ + { + label: editContactUuid ? 'Save Contact' : 'Add Contact', + onClick: handleSubmit, + type: 'primary', + mobileSlot: 'right', + }, + { + label: 'Cancel', + onClick: handleDialogClose, + type: 'cancel', + mobileSlot: 'left', + }, + ], + [editContactUuid, handleDialogClose, handleSubmit], + ) + + return ( + +
+
+
+ { + setName(value) + }} + /> + + { + setCollaborationID(value) + }} + /> + + {!editContactUuid && ( +

+ Ask your contact for their Standard Notes CollaborationID via secure email or chat. Then, enter it here + to add them as a contact. +

+ )} +
+
+
+
+ ) +} + +export default EditContactModal diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/ContactInviteModal.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/ContactInviteModal.tsx new file mode 100644 index 000000000..3804c6c1f --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/ContactInviteModal.tsx @@ -0,0 +1,97 @@ +import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react' +import Modal, { ModalAction } from '@/Components/Modal/Modal' +import { useApplication } from '@/Components/ApplicationProvider' +import { SharedVaultPermission, SharedVaultListingInterface, TrustedContactInterface } from '@standardnotes/snjs' + +type Props = { + vault: SharedVaultListingInterface + onCloseDialog: () => void +} + +const ContactInviteModal: FunctionComponent = ({ vault, onCloseDialog }) => { + const application = useApplication() + + const [selectedContacts, setSelectedContacts] = useState([]) + const [contacts, setContacts] = useState([]) + + useEffect(() => { + const loadContacts = async () => { + const contacts = await application.sharedVaults.getInvitableContactsForSharedVault(vault) + setContacts(contacts) + } + void loadContacts() + }, [application.sharedVaults, vault]) + + const handleDialogClose = useCallback(() => { + onCloseDialog() + }, [onCloseDialog]) + + const inviteSelectedContacts = useCallback(async () => { + for (const contact of selectedContacts) { + await application.sharedVaults.inviteContactToSharedVault(vault, contact, SharedVaultPermission.Write) + } + handleDialogClose() + }, [application.sharedVaults, vault, handleDialogClose, selectedContacts]) + + const toggleContact = useCallback( + (contact: TrustedContactInterface) => { + if (selectedContacts.includes(contact)) { + const index = selectedContacts.indexOf(contact) + const updatedContacts = [...selectedContacts] + updatedContacts.splice(index, 1) + setSelectedContacts(updatedContacts) + } else { + setSelectedContacts([...selectedContacts, contact]) + } + }, + [selectedContacts, setSelectedContacts], + ) + + const modalActions = useMemo( + (): ModalAction[] => [ + { + label: 'Invite Selected Contacts', + onClick: inviteSelectedContacts, + type: 'primary', + mobileSlot: 'right', + }, + { + label: 'Cancel', + onClick: handleDialogClose, + type: 'cancel', + mobileSlot: 'left', + }, + ], + [handleDialogClose, inviteSelectedContacts], + ) + + return ( + +
+
+
+ {contacts.map((contact) => { + return ( +
toggleContact(contact)}> +
+ toggleContact(contact)} + /> +
+
+ {contact.name} + {contact.contactUuid} +
+
+ ) + })} +
+
+
+
+ ) +} + +export default ContactInviteModal diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/InviteItem.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/InviteItem.tsx new file mode 100644 index 000000000..4e77932b7 --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Invites/InviteItem.tsx @@ -0,0 +1,67 @@ +import { useApplication } from '@/Components/ApplicationProvider' +import Button from '@/Components/Button/Button' +import Icon from '@/Components/Icon/Icon' +import ModalOverlay from '@/Components/Modal/ModalOverlay' +import { PendingSharedVaultInviteRecord } from '@standardnotes/snjs' +import { useCallback, useState } from 'react' +import EditContactModal from '../Contacts/EditContactModal' + +type Props = { + invite: PendingSharedVaultInviteRecord +} + +const InviteItem = ({ invite }: Props) => { + const application = useApplication() + const [isAddContactModalOpen, setIsAddContactModalOpen] = useState(false) + + const isTrusted = invite.trusted + const inviteData = invite.message.data + + const addAsTrustedContact = useCallback(() => { + setIsAddContactModalOpen(true) + }, []) + + const acceptInvite = useCallback(async () => { + await application.sharedVaults.acceptPendingSharedVaultInvite(invite) + }, [application.sharedVaults, invite]) + + const closeAddContactModal = () => setIsAddContactModalOpen(false) + const collaborationId = application.contacts.getCollaborationIDFromInvite(invite.invite) + + return ( + <> + + + + +
+ +
+ Vault Name: {inviteData.metadata.name} + + Vault Description: {inviteData.metadata.description} + + + Sender CollaborationID: {collaborationId} + + +
+ {isTrusted ? ( +
+ )} +
+
+
+ + ) +} + +export default InviteItem diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx new file mode 100644 index 000000000..bb0a9b671 --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults.tsx @@ -0,0 +1,157 @@ +import { observer } from 'mobx-react-lite' +import { Subtitle, Title } from '@/Components/Preferences/PreferencesComponents/Content' +import PreferencesGroup from '@/Components/Preferences/PreferencesComponents/PreferencesGroup' +import PreferencesSegment from '@/Components/Preferences/PreferencesComponents/PreferencesSegment' +import { useApplication } from '@/Components/ApplicationProvider' +import ContactItem from './Contacts/ContactItem' +import ModalOverlay from '@/Components/Modal/ModalOverlay' +import EditContactModal from './Contacts/EditContactModal' +import { useCallback, useEffect, useState } from 'react' +import { + VaultListingInterface, + TrustedContactInterface, + PendingSharedVaultInviteRecord, + ContentType, + SharedVaultServiceEvent, +} from '@standardnotes/snjs' +import VaultItem from './Vaults/VaultItem' +import Button from '@/Components/Button/Button' +import InviteItem from './Invites/InviteItem' +import EditVaultModal from './Vaults/VaultModal/EditVaultModal' + +const Vaults = () => { + const application = useApplication() + + const [vaults, setVaults] = useState([]) + const [invites, setInvites] = useState([]) + const [contacts, setContacts] = useState([]) + + const [isAddContactModalOpen, setIsAddContactModalOpen] = useState(false) + const closeAddContactModal = () => setIsAddContactModalOpen(false) + + const [isVaultModalOpen, setIsVaultModalOpen] = useState(false) + const closeVaultModal = () => setIsVaultModalOpen(false) + + const vaultService = application.vaults + const sharedVaultService = application.sharedVaults + const contactService = application.contacts + + const updateVaults = useCallback(async () => { + setVaults(vaultService.getVaults()) + }, [vaultService]) + + const fetchInvites = useCallback(async () => { + await sharedVaultService.downloadInboundInvites() + const invites = sharedVaultService.getCachedPendingInviteRecords() + setInvites(invites) + }, [sharedVaultService]) + + const updateContacts = useCallback(async () => { + setContacts(contactService.getAllContacts()) + }, [contactService]) + + useEffect(() => { + return application.sharedVaults.addEventObserver((event) => { + if (event === SharedVaultServiceEvent.SharedVaultStatusChanged) { + void fetchInvites() + } + }) + }) + + useEffect(() => { + return application.streamItems([ContentType.VaultListing, ContentType.TrustedContact], () => { + void updateVaults() + void fetchInvites() + void updateContacts() + }) + }, [application, updateVaults, fetchInvites, updateContacts]) + + const createNewVault = useCallback(async () => { + setIsVaultModalOpen(true) + }, []) + + const createNewContact = useCallback(() => { + setIsAddContactModalOpen(true) + }, []) + + useEffect(() => { + void updateVaults() + void fetchInvites() + void updateContacts() + }, [updateContacts, updateVaults, fetchInvites]) + + return ( + <> + + + + + + + + + + + Incoming Invites +
+ {invites.map((invite) => { + return + })} +
+
+
+ + + + Contacts +
+ {contacts.map((contact) => { + return + })} +
+
+
+
+
+ + + + Vaults +
+ {vaults.map((vault) => { + return + })} +
+
+
+
+
+ + + + CollaborationID + Share your CollaborationID with collaborators to join their vaults. + {contactService.isCollaborationEnabled() ? ( +
+ +
{contactService.getCollaborationID()}
+
+
+ ) : ( +
+
+ )} +
+
+ + ) +} + +export default observer(Vaults) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultItem.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultItem.tsx new file mode 100644 index 000000000..e3a40a484 --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultItem.tsx @@ -0,0 +1,146 @@ +import { useApplication } from '@/Components/ApplicationProvider' +import Button from '@/Components/Button/Button' +import Icon from '@/Components/Icon/Icon' +import ModalOverlay from '@/Components/Modal/ModalOverlay' +import { ButtonType, VaultListingInterface, isClientDisplayableError } from '@standardnotes/snjs' +import { useCallback, useState } from 'react' +import ContactInviteModal from '../Invites/ContactInviteModal' +import EditVaultModal from './VaultModal/EditVaultModal' + +type Props = { + vault: VaultListingInterface +} + +const VaultItem = ({ vault }: Props) => { + const application = useApplication() + + const [isInviteModalOpen, setIsAddContactModalOpen] = useState(false) + const closeInviteModal = () => setIsAddContactModalOpen(false) + + const [isVaultModalOpen, setIsVaultModalOpen] = useState(false) + const closeVaultModal = () => setIsVaultModalOpen(false) + + const isAdmin = !vault.isSharedVaultListing() ? true : application.sharedVaults.isCurrentUserSharedVaultAdmin(vault) + + const deleteVault = useCallback(async () => { + const confirm = await application.alerts.confirm( + 'Deleting a vault will permanently delete all its items and files.', + 'Are you sure you want to delete this vault?', + undefined, + ButtonType.Danger, + ) + if (!confirm) { + return + } + + if (vault.isSharedVaultListing()) { + const result = await application.sharedVaults.deleteSharedVault(vault) + if (isClientDisplayableError(result)) { + void application.alerts.showErrorAlert(result) + } + } else { + const success = await application.vaults.deleteVault(vault) + if (!success) { + void application.alerts.alert('Unable to delete vault. Please try again.') + } + } + }, [application.alerts, application.sharedVaults, application.vaults, vault]) + + const leaveVault = useCallback(async () => { + if (!vault.isSharedVaultListing()) { + return + } + + const confirm = await application.alerts.confirm( + 'All items and files in this vault will be removed from your account.', + 'Are you sure you want to leave this vault?', + undefined, + ButtonType.Danger, + ) + if (!confirm) { + return + } + + const success = await application.sharedVaults.leaveSharedVault(vault) + if (!success) { + void application.alerts.alert('Unable to leave vault. Please try again.') + } + }, [application.alerts, application.sharedVaults, vault]) + + const convertToSharedVault = useCallback(async () => { + await application.sharedVaults.convertVaultToSharedVault(vault) + }, [application.sharedVaults, vault]) + + const ensureVaultIsUnlocked = useCallback(async () => { + if (!application.vaults.isVaultLocked(vault)) { + return true + } + const unlocked = await application.vaultDisplayService.unlockVault(vault) + return unlocked + }, [application.vaultDisplayService, application.vaults, vault]) + + const openEditModal = useCallback(async () => { + if (!(await ensureVaultIsUnlocked())) { + return + } + + setIsVaultModalOpen(true) + }, [ensureVaultIsUnlocked]) + + const openInviteModal = useCallback(async () => { + if (!(await ensureVaultIsUnlocked())) { + return + } + + setIsAddContactModalOpen(true) + }, [ensureVaultIsUnlocked]) + + return ( + <> + {vault.isSharedVaultListing() && ( + + + + )} + + + + + +
+ +
+ {vault.name} + {vault.description} + Vault ID: {vault.systemIdentifier} + +
+
+
+
+ {vault.isSharedVaultListing() ? ( +
+
+
+
+ + ) +} + +export default VaultItem diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/EditVaultModal.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/EditVaultModal.tsx new file mode 100644 index 000000000..c1233f24d --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/EditVaultModal.tsx @@ -0,0 +1,224 @@ +import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react' +import Modal, { ModalAction } from '@/Components/Modal/Modal' +import DecoratedInput from '@/Components/Input/DecoratedInput' +import { useApplication } from '@/Components/ApplicationProvider' +import { + ChangeVaultOptionsDTO, + KeySystemRootKeyPasswordType, + KeySystemRootKeyStorageMode, + SharedVaultInviteServerHash, + SharedVaultUserServerHash, + VaultListingInterface, + isClientDisplayableError, +} from '@standardnotes/snjs' +import { VaultModalMembers } from './VaultModalMembers' +import { VaultModalInvites } from './VaultModalInvites' +import { PasswordTypePreference } from './PasswordTypePreference' +import { KeyStoragePreference } from './KeyStoragePreference' +import useItem from '@/Hooks/useItem' + +type Props = { + existingVaultUuid?: string + onCloseDialog: () => void +} + +const EditVaultModal: FunctionComponent = ({ onCloseDialog, existingVaultUuid }) => { + const application = useApplication() + + const existingVault = useItem(existingVaultUuid) + + const [name, setName] = useState('') + const [description, setDescription] = useState('') + const [members, setMembers] = useState([]) + const [invites, setInvites] = useState([]) + const [isAdmin, setIsAdmin] = useState(true) + const [passwordType, setPasswordType] = useState( + KeySystemRootKeyPasswordType.Randomized, + ) + const [keyStorageMode, setKeyStorageMode] = useState(KeySystemRootKeyStorageMode.Synced) + const [customPassword, setCustomPassword] = useState(undefined) + + useEffect(() => { + if (existingVault) { + setName(existingVault.name ?? '') + setDescription(existingVault.description ?? '') + setPasswordType(existingVault.rootKeyParams.passwordType) + setKeyStorageMode(existingVault.keyStorageMode) + } + }, [application.vaults, existingVault]) + + const reloadVaultInfo = useCallback(async () => { + if (!existingVault) { + return + } + + if (existingVault.isSharedVaultListing()) { + setIsAdmin( + existingVault.isSharedVaultListing() && application.sharedVaults.isCurrentUserSharedVaultAdmin(existingVault), + ) + + const users = await application.sharedVaults.getSharedVaultUsers(existingVault) + if (users) { + setMembers(users) + } + + const invites = await application.sharedVaults.getOutboundInvites(existingVault) + if (!isClientDisplayableError(invites)) { + setInvites(invites) + } + } + }, [application.sharedVaults, existingVault]) + + useEffect(() => { + void reloadVaultInfo() + }, [application.vaults, reloadVaultInfo]) + + const handleDialogClose = useCallback(() => { + onCloseDialog() + }, [onCloseDialog]) + + const saveExistingVault = useCallback( + async (vault: VaultListingInterface) => { + if (vault.name !== name || vault.description !== description) { + await application.vaults.changeVaultNameAndDescription(vault, { + name: name, + description: description, + }) + } + + const isChangingPasswordType = vault.keyPasswordType !== passwordType + const isChangingKeyStorageMode = vault.keyStorageMode !== keyStorageMode + + const getPasswordTypeParams = (): ChangeVaultOptionsDTO['newPasswordType'] => { + if (!isChangingPasswordType) { + throw new Error('Password type is not changing') + } + + if (passwordType === KeySystemRootKeyPasswordType.UserInputted) { + if (!customPassword) { + throw new Error('Custom password is not set') + } + return { + passwordType, + userInputtedPassword: customPassword, + } + } else { + return { + passwordType, + } + } + } + + if (isChangingPasswordType || isChangingKeyStorageMode) { + await application.vaults.changeVaultOptions({ + vault, + newPasswordType: isChangingPasswordType ? getPasswordTypeParams() : undefined, + newKeyStorageMode: isChangingKeyStorageMode ? keyStorageMode : undefined, + }) + } + }, + [application.vaults, customPassword, description, keyStorageMode, name, passwordType], + ) + + const createNewVault = useCallback(async () => { + if (passwordType === KeySystemRootKeyPasswordType.UserInputted) { + if (!customPassword) { + throw new Error('Custom key is not set') + } + await application.vaults.createUserInputtedPasswordVault({ + name, + description, + storagePreference: keyStorageMode, + userInputtedPassword: customPassword, + }) + } else { + await application.vaults.createRandomizedVault({ + name, + description, + storagePreference: keyStorageMode, + }) + } + + handleDialogClose() + }, [application.vaults, customPassword, description, handleDialogClose, keyStorageMode, name, passwordType]) + + const handleSubmit = useCallback(async () => { + if (existingVault) { + await saveExistingVault(existingVault) + } else { + await createNewVault() + } + handleDialogClose() + }, [existingVault, handleDialogClose, saveExistingVault, createNewVault]) + + const modalActions = useMemo( + (): ModalAction[] => [ + { + label: existingVault ? 'Save Vault' : 'Create Vault', + onClick: handleSubmit, + type: 'primary', + mobileSlot: 'right', + }, + { + label: 'Cancel', + onClick: handleDialogClose, + type: 'cancel', + mobileSlot: 'left', + }, + ], + [existingVault, handleDialogClose, handleSubmit], + ) + + if (existingVault && application.vaults.isVaultLocked(existingVault)) { + return
Vault is locked.
+ } + + return ( + +
+
+
+
Vault Info
+
The vault name and description are end-to-end encrypted.
+ + { + setName(value) + }} + /> + + { + setDescription(value) + }} + /> +
+ + {existingVault && ( + + )} + + {existingVault && } + + + + +
+
+
+ ) +} + +export default EditVaultModal diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/KeyStoragePreference.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/KeyStoragePreference.tsx new file mode 100644 index 000000000..b0c191651 --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/KeyStoragePreference.tsx @@ -0,0 +1,57 @@ +import { KeySystemRootKeyStorageMode } from '@standardnotes/snjs' +import StyledRadioInput from '@/Components/Radio/StyledRadioInput' + +type KeyStorageOption = { + value: KeySystemRootKeyStorageMode + label: string + description: string +} + +const options: KeyStorageOption[] = [ + { + value: KeySystemRootKeyStorageMode.Synced, + label: 'Synced (Recommended)', + description: + 'Your vault key will be encrypted and synced to your account and automatically available on your other devices.', + }, + { + value: KeySystemRootKeyStorageMode.Local, + label: 'Local', + description: + 'Your vault key will be encrypted and saved locally on this device. You will need to manually enter your vault key on your other devices.', + }, + { + value: KeySystemRootKeyStorageMode.Ephemeral, + label: 'Ephemeral', + description: + 'Your vault key will only be stored in memory and will be forgotten when you close the app. You will need to manually enter your vault key on your other devices.', + }, +] + +export const KeyStoragePreference = ({ + value, + onChange, +}: { + value: KeySystemRootKeyStorageMode + onChange: (value: KeySystemRootKeyStorageMode) => void +}) => { + return ( +
+
Vault Key Type
+ {options.map((option) => { + return ( + + ) + })} +
+ ) +} diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/PasswordTypePreference.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/PasswordTypePreference.tsx new file mode 100644 index 000000000..0b6a7857c --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/PasswordTypePreference.tsx @@ -0,0 +1,75 @@ +import { KeySystemRootKeyPasswordType } from '@standardnotes/snjs' +import StyledRadioInput from '@/Components/Radio/StyledRadioInput' +import DecoratedPasswordInput from '@/Components/Input/DecoratedPasswordInput' +import { useState } from 'react' + +type PasswordTypePreference = { + value: KeySystemRootKeyPasswordType + label: string + description: string +} + +const options: PasswordTypePreference[] = [ + { + value: KeySystemRootKeyPasswordType.Randomized, + label: 'Randomized (Recommended)', + description: 'Your vault key will be randomly generated and synced to your account.', + }, + { + value: KeySystemRootKeyPasswordType.UserInputted, + label: 'Custom (Advanced)', + description: + 'Choose your own key for your vault. This is an advanced option and is not recommended for most users.', + }, +] + +export const PasswordTypePreference = ({ + value, + onChange, + onCustomKeyChange, +}: { + value: KeySystemRootKeyPasswordType + onChange: (value: KeySystemRootKeyPasswordType) => void + onCustomKeyChange: (value: string) => void +}) => { + const [customKey, setCustomKey] = useState('') + + const onKeyInputChange = (value: string) => { + setCustomKey(value) + onCustomKeyChange(value) + } + + return ( +
+
Vault Key Type
+ {options.map((option) => { + return ( + + ) + })} + + {value === KeySystemRootKeyPasswordType.UserInputted && ( +
+
{options[1].description}
+ + +
+ )} +
+ ) +} diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalInvites.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalInvites.tsx new file mode 100644 index 000000000..d0df96dda --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalInvites.tsx @@ -0,0 +1,57 @@ +import { useCallback } from 'react' +import { useApplication } from '@/Components/ApplicationProvider' +import { SharedVaultInviteServerHash } from '@standardnotes/snjs' +import Icon from '@/Components/Icon/Icon' +import Button from '@/Components/Button/Button' + +export const VaultModalInvites = ({ + invites, + onChange, + isAdmin, +}: { + invites: SharedVaultInviteServerHash[] + onChange: () => void + isAdmin: boolean +}) => { + const application = useApplication() + + const deleteInvite = useCallback( + async (invite: SharedVaultInviteServerHash) => { + await application.sharedVaults.deleteInvite(invite) + onChange() + }, + [application.sharedVaults, onChange], + ) + + return ( +
+
Pending Invites
+ {invites.map((invite) => { + const contact = application.contacts.findTrustedContactForInvite(invite) + + return ( +
+ +
+ + {contact?.name || invite.user_uuid} + + {contact && Trusted} + {!contact && ( +
+ Untrusted +
+ )} + + {isAdmin && ( +
+
+ )} +
+
+ ) + })} +
+ ) +} diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalMembers.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalMembers.tsx new file mode 100644 index 000000000..f6a007fe6 --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Vaults/Vaults/VaultModal/VaultModalMembers.tsx @@ -0,0 +1,71 @@ +import { useCallback } from 'react' +import { useApplication } from '@/Components/ApplicationProvider' +import { SharedVaultUserServerHash, VaultListingInterface } from '@standardnotes/snjs' +import Icon from '@/Components/Icon/Icon' +import Button from '@/Components/Button/Button' + +export const VaultModalMembers = ({ + members, + isAdmin, + vault, + onChange, +}: { + members: SharedVaultUserServerHash[] + vault: VaultListingInterface + isAdmin: boolean + onChange: () => void +}) => { + const application = useApplication() + + const removeMemberFromVault = useCallback( + async (memberItem: SharedVaultUserServerHash) => { + if (vault.isSharedVaultListing()) { + await application.sharedVaults.removeUserFromSharedVault(vault, memberItem.user_uuid) + onChange() + } + }, + [application.sharedVaults, vault, onChange], + ) + + return ( +
+
Vault Members
+ {members.map((member) => { + if (application.sharedVaults.isSharedVaultUserSharedVaultOwner(member)) { + return null + } + + const contact = application.contacts.findTrustedContactForServerUser(member) + return ( +
+ +
+ + {contact?.name || member.user_uuid} + + {contact && Trusted} + {!contact && ( +
+ Untrusted +
+ )} + + {isAdmin && ( +
+
+ )} +
+
+ ) + })} +
+ ) +} diff --git a/packages/web/src/javascripts/Components/Preferences/PreferencesMenu.ts b/packages/web/src/javascripts/Components/Preferences/PreferencesMenu.ts index e86cd0adb..4e121a4a6 100644 --- a/packages/web/src/javascripts/Components/Preferences/PreferencesMenu.ts +++ b/packages/web/src/javascripts/Components/Preferences/PreferencesMenu.ts @@ -4,6 +4,7 @@ import { WebApplication } from '@/Application/WebApplication' import { PackageProvider } from './Panes/General/Advanced/Packages/Provider/PackageProvider' import { securityPrefsHasBubble } from './Panes/Security/securityPrefsHasBubble' import { PreferenceId } from '@standardnotes/ui-services' +import { featureTrunkVaultsEnabled } from '@/FeatureTrunk' interface PreferencesMenuItem { readonly id: PreferenceId @@ -44,6 +45,11 @@ const READY_PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [ { id: 'help-feedback', label: 'Help & feedback', icon: 'help' }, ] +if (featureTrunkVaultsEnabled()) { + PREFERENCES_MENU_ITEMS.splice(3, 0, { id: 'vaults', label: 'Vaults', icon: 'safe-square' }) + READY_PREFERENCES_MENU_ITEMS.splice(3, 0, { id: 'vaults', label: 'Vaults', icon: 'safe-square' }) +} + export class PreferencesMenu { private _selectedPane: PreferenceId = 'account' private _menu: PreferencesMenuItem[] diff --git a/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx b/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx index deca7ea20..f561d2858 100644 --- a/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx +++ b/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx @@ -102,9 +102,9 @@ const QuickSettingsMenu: FunctionComponent = ({ application, quickSet const toggleComponent = useCallback( (component: SNComponent) => { if (component.isTheme()) { - application.mutator.toggleTheme(component).catch(console.error) + application.componentManager.toggleTheme(component.uuid).catch(console.error) } else { - application.mutator.toggleComponent(component).catch(console.error) + application.componentManager.toggleComponent(component.uuid).catch(console.error) } }, [application], @@ -113,7 +113,7 @@ const QuickSettingsMenu: FunctionComponent = ({ application, quickSet const deactivateAnyNonLayerableTheme = useCallback(() => { const activeTheme = themes.map((item) => item.component).find((theme) => theme?.active && !theme.isLayerable()) if (activeTheme) { - application.mutator.toggleTheme(activeTheme).catch(console.error) + application.componentManager.toggleTheme(activeTheme.uuid).catch(console.error) } }, [application, themes]) diff --git a/packages/web/src/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx b/packages/web/src/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx index 130d95d99..a5cdcd85f 100644 --- a/packages/web/src/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx +++ b/packages/web/src/javascripts/Components/QuickSettingsMenu/ThemesMenuButton.tsx @@ -38,7 +38,7 @@ const ThemesMenuButton: FunctionComponent = ({ application, item }) => { const themeIsLayerableOrNotActive = isThemeLayerable || !item.component.active if (themeIsLayerableOrNotActive) { - application.mutator.toggleTheme(item.component).catch(console.error) + application.componentManager.toggleTheme(item.component.uuid).catch(console.error) } } else { premiumModal.activate(`${item.name} theme`) diff --git a/packages/web/src/javascripts/Components/RadioButtonGroup/RadioButtonGroup.tsx b/packages/web/src/javascripts/Components/RadioButtonGroup/RadioButtonGroup.tsx index cac192103..c79904c75 100644 --- a/packages/web/src/javascripts/Components/RadioButtonGroup/RadioButtonGroup.tsx +++ b/packages/web/src/javascripts/Components/RadioButtonGroup/RadioButtonGroup.tsx @@ -5,9 +5,10 @@ type Props = { items: { label: string; value: string }[] value: string onChange: (value: string) => void + className?: string } -const RadioButtonGroup = ({ value, items, onChange }: Props) => { +const RadioButtonGroup = ({ value, items, onChange, className }: Props) => { const radio = useRadioStore({ value, orientation: 'horizontal', @@ -17,7 +18,7 @@ const RadioButtonGroup = ({ value, items, onChange }: Props) => { }) return ( - + {items.map(({ label, value: itemValue }) => (

m4$ZZDDsAs-$PWW5swMgK$nIaJjJcC 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 B%1HnK literal 0 HcmV?d00001 diff --git a/.yarn/cache/@typescript-eslint-typescript-estree-npm-5.59.9-ec2ce6608c-c0c9b81f20.zip b/.yarn/cache/@typescript-eslint-typescript-estree-npm-5.59.9-ec2ce6608c-c0c9b81f20.zip new file mode 100644 index 0000000000000000000000000000000000000000..4b11235c7950c46b04a595e9f3b9d183ad90dd17 GIT binary patch literal 183638 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 Q{`&;hk+dB^<^bvc01uj$6aWAK literal 0 HcmV?d00001 diff --git a/.yarn/cache/@typescript-eslint-utils-npm-5.59.9-d1ab6a9f9a-22ec596288.zip b/.yarn/cache/@typescript-eslint-utils-npm-5.59.9-d1ab6a9f9a-22ec596288.zip new file mode 100644 index 0000000000000000000000000000000000000000..da02c617d4463222b9ba8226366bb233e234863a GIT binary patch literal 178744 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�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�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 HGe!9iGk)uM literal 0 HcmV?d00001 diff --git a/.yarn/cache/@typescript-eslint-visitor-keys-npm-5.59.9-3e52021052-2909ce761f.zip b/.yarn/cache/@typescript-eslint-visitor-keys-npm-5.59.9-3e52021052-2909ce761f.zip new file mode 100644 index 0000000000000000000000000000000000000000..42288247e5b0fcd7edf8408f690df777780464e6 GIT binary patch literal 11298 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 @@ -35,7 +35,7 @@ export class UserApiService implements UserApiServiceInterface { return response } catch (error) { - throw new ApiCallError(ErrorMessage.GenericRegistrationFail) + throw new ApiCallError(ErrorMessage.GenericFail) } } @@ -84,6 +84,23 @@ export class UserApiService implements UserApiServiceInterface { } } + async updateUser(updateDTO: { userUuid: string }): Promise> { + this.lockOperation(UserApiOperations.UpdatingUser) + + try { + const response = await this.userServer.update({ + [ApiEndpointParam.ApiVersion]: ApiVersion.v0, + user_uuid: updateDTO.userUuid, + }) + + this.unlockOperation(UserApiOperations.UpdatingUser) + + return response + } catch (error) { + throw new ApiCallError(ErrorMessage.GenericFail) + } + } + private lockOperation(operation: UserApiOperations): void { if (this.operationsInProgress.get(operation)) { throw new ApiCallError(ErrorMessage.GenericInProgress) diff --git a/packages/api/src/Domain/Client/User/UserApiServiceInterface.ts b/packages/api/src/Domain/Client/User/UserApiServiceInterface.ts index e951b7f62..f10f798a4 100644 --- a/packages/api/src/Domain/Client/User/UserApiServiceInterface.ts +++ b/packages/api/src/Domain/Client/User/UserApiServiceInterface.ts @@ -5,6 +5,7 @@ import { HttpResponse } from '@standardnotes/responses' import { UserDeletionResponseBody } from '../../Response/User/UserDeletionResponseBody' import { UserRegistrationResponseBody } from '../../Response/User/UserRegistrationResponseBody' import { UserRequestResponseBody } from '../../Response/UserRequest/UserRequestResponseBody' +import { UserUpdateResponse } from '../../Response/User/UserUpdateResponse' export interface UserApiServiceInterface { register(registerDTO: { @@ -13,9 +14,12 @@ export interface UserApiServiceInterface { keyParams: RootKeyParamsInterface ephemeral: boolean }): Promise> + updateUser(updateDTO: { userUuid: string }): Promise> + submitUserRequest(dto: { userUuid: string requestType: UserRequestType }): Promise> + deleteAccount(userUuid: string): Promise> } diff --git a/packages/api/src/Domain/Http/HttpService.ts b/packages/api/src/Domain/Http/HttpService.ts index 2cda7e8ca..3e76b2e5a 100644 --- a/packages/api/src/Domain/Http/HttpService.ts +++ b/packages/api/src/Domain/Http/HttpService.ts @@ -53,7 +53,15 @@ export class HttpService implements HttpServiceInterface { this.host = host } + getHost(): string { + return this.host + } + async get(path: string, params?: HttpRequestParams, authentication?: string): Promise> { + if (!this.host) { + throw new Error('Attempting to make network request before host is set') + } + return this.runHttp({ url: joinPaths(this.host, path), params, @@ -62,7 +70,20 @@ export class HttpService implements HttpServiceInterface { }) } + async getExternal(url: string, params?: HttpRequestParams): Promise> { + return this.runHttp({ + url, + params, + verb: HttpVerb.Get, + external: true, + }) + } + async post(path: string, params?: HttpRequestParams, authentication?: string): Promise> { + if (!this.host) { + throw new Error('Attempting to make network request before host is set') + } + return this.runHttp({ url: joinPaths(this.host, path), params, diff --git a/packages/api/src/Domain/Http/HttpServiceInterface.ts b/packages/api/src/Domain/Http/HttpServiceInterface.ts index cff0fd875..2db8ff58c 100644 --- a/packages/api/src/Domain/Http/HttpServiceInterface.ts +++ b/packages/api/src/Domain/Http/HttpServiceInterface.ts @@ -3,8 +3,10 @@ import { HttpRequest, HttpRequestParams, HttpResponse, HttpResponseMeta } from ' export interface HttpServiceInterface { setHost(host: string): void + getHost(): string setSession(session: Session): void get(path: string, params?: HttpRequestParams, authentication?: string): Promise> + getExternal(url: string, params?: HttpRequestParams): Promise> post(path: string, params?: HttpRequestParams, authentication?: string): Promise> put(path: string, params?: HttpRequestParams, authentication?: string): Promise> patch(path: string, params: HttpRequestParams, authentication?: string): Promise> diff --git a/packages/api/src/Domain/Request/ApiEndpointParam.ts b/packages/api/src/Domain/Request/ApiEndpointParam.ts deleted file mode 100644 index 007ded326..000000000 --- a/packages/api/src/Domain/Request/ApiEndpointParam.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum ApiEndpointParam { - LastSyncToken = 'sync_token', - PaginationToken = 'cursor_token', - SyncDlLimit = 'limit', - SyncPayloads = 'items', - ApiVersion = 'api', -} diff --git a/packages/api/src/Domain/Request/AsymmetricMessage/CreateAsymmetricMessageParams.ts b/packages/api/src/Domain/Request/AsymmetricMessage/CreateAsymmetricMessageParams.ts new file mode 100644 index 000000000..bcaec0517 --- /dev/null +++ b/packages/api/src/Domain/Request/AsymmetricMessage/CreateAsymmetricMessageParams.ts @@ -0,0 +1,5 @@ +export type CreateAsymmetricMessageParams = { + recipientUuid: string + encryptedMessage: string + replaceabilityIdentifier?: string +} diff --git a/packages/api/src/Domain/Request/AsymmetricMessage/DeleteAsymmetricMessageRequestParams.ts b/packages/api/src/Domain/Request/AsymmetricMessage/DeleteAsymmetricMessageRequestParams.ts new file mode 100644 index 000000000..5aab92dbc --- /dev/null +++ b/packages/api/src/Domain/Request/AsymmetricMessage/DeleteAsymmetricMessageRequestParams.ts @@ -0,0 +1,3 @@ +export type DeleteAsymmetricMessageRequestParams = { + messageUuid: string +} diff --git a/packages/api/src/Domain/Request/AsymmetricMessage/GetOutboundAsymmetricMessagesRequestParams.ts b/packages/api/src/Domain/Request/AsymmetricMessage/GetOutboundAsymmetricMessagesRequestParams.ts new file mode 100644 index 000000000..3ccd1dbce --- /dev/null +++ b/packages/api/src/Domain/Request/AsymmetricMessage/GetOutboundAsymmetricMessagesRequestParams.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/ban-types +export type GetOutboundAsymmetricMessagesRequestParams = {} diff --git a/packages/api/src/Domain/Request/AsymmetricMessage/GetUserAsymmetricMessagesRequestParams.ts b/packages/api/src/Domain/Request/AsymmetricMessage/GetUserAsymmetricMessagesRequestParams.ts new file mode 100644 index 000000000..da4d3f813 --- /dev/null +++ b/packages/api/src/Domain/Request/AsymmetricMessage/GetUserAsymmetricMessagesRequestParams.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/ban-types +export type GetUserAsymmetricMessagesRequestParams = {} diff --git a/packages/api/src/Domain/Request/AsymmetricMessage/UpdateAsymmetricMessageParams.ts b/packages/api/src/Domain/Request/AsymmetricMessage/UpdateAsymmetricMessageParams.ts new file mode 100644 index 000000000..f1c5757fa --- /dev/null +++ b/packages/api/src/Domain/Request/AsymmetricMessage/UpdateAsymmetricMessageParams.ts @@ -0,0 +1,4 @@ +export type UpdateAsymmetricMessageParams = { + messageUuid: string + encryptedMessage: string +} diff --git a/packages/api/src/Domain/Request/SharedVault/CreateSharedVaultValetTokenParams.ts b/packages/api/src/Domain/Request/SharedVault/CreateSharedVaultValetTokenParams.ts new file mode 100644 index 000000000..60a8833a6 --- /dev/null +++ b/packages/api/src/Domain/Request/SharedVault/CreateSharedVaultValetTokenParams.ts @@ -0,0 +1,12 @@ +import { ValetTokenOperation } from '@standardnotes/responses' +import { SharedVaultMoveType } from './SharedVaultMoveType' + +export type CreateSharedVaultValetTokenParams = { + sharedVaultUuid: string + fileUuid?: string + remoteIdentifier: string + operation: ValetTokenOperation + unencryptedFileSize?: number + moveOperationType?: SharedVaultMoveType + sharedVaultToSharedVaultMoveTargetUuid?: string +} diff --git a/packages/api/src/Domain/Request/SharedVault/SharedVaultMoveType.ts b/packages/api/src/Domain/Request/SharedVault/SharedVaultMoveType.ts new file mode 100644 index 000000000..07b24b13d --- /dev/null +++ b/packages/api/src/Domain/Request/SharedVault/SharedVaultMoveType.ts @@ -0,0 +1 @@ +export type SharedVaultMoveType = 'shared-vault-to-user' | 'user-to-shared-vault' | 'shared-vault-to-shared-vault' diff --git a/packages/api/src/Domain/Request/SharedVaultInvites/AcceptInviteRequestParams.ts b/packages/api/src/Domain/Request/SharedVaultInvites/AcceptInviteRequestParams.ts new file mode 100644 index 000000000..a88c62a90 --- /dev/null +++ b/packages/api/src/Domain/Request/SharedVaultInvites/AcceptInviteRequestParams.ts @@ -0,0 +1,4 @@ +export type AcceptInviteRequestParams = { + sharedVaultUuid: string + inviteUuid: string +} diff --git a/packages/api/src/Domain/Request/SharedVaultInvites/CreateSharedVaultInviteParams.ts b/packages/api/src/Domain/Request/SharedVaultInvites/CreateSharedVaultInviteParams.ts new file mode 100644 index 000000000..ba8332aae --- /dev/null +++ b/packages/api/src/Domain/Request/SharedVaultInvites/CreateSharedVaultInviteParams.ts @@ -0,0 +1,8 @@ +import { SharedVaultPermission } from '@standardnotes/responses' + +export type CreateSharedVaultInviteParams = { + sharedVaultUuid: string + recipientUuid: string + encryptedMessage: string + permissions: SharedVaultPermission +} diff --git a/packages/api/src/Domain/Request/SharedVaultInvites/DeclineInviteRequestParams.ts b/packages/api/src/Domain/Request/SharedVaultInvites/DeclineInviteRequestParams.ts new file mode 100644 index 000000000..2feb8cca2 --- /dev/null +++ b/packages/api/src/Domain/Request/SharedVaultInvites/DeclineInviteRequestParams.ts @@ -0,0 +1,4 @@ +export type DeclineInviteRequestParams = { + sharedVaultUuid: string + inviteUuid: string +} diff --git a/packages/api/src/Domain/Request/SharedVaultInvites/DeleteAllSharedVaultInvitesRequestParams.ts b/packages/api/src/Domain/Request/SharedVaultInvites/DeleteAllSharedVaultInvitesRequestParams.ts new file mode 100644 index 000000000..0aa027aa0 --- /dev/null +++ b/packages/api/src/Domain/Request/SharedVaultInvites/DeleteAllSharedVaultInvitesRequestParams.ts @@ -0,0 +1,3 @@ +export type DeleteAllSharedVaultInvitesRequestParams = { + sharedVaultUuid: string +} diff --git a/packages/api/src/Domain/Request/SharedVaultInvites/DeleteInviteRequestParams.ts b/packages/api/src/Domain/Request/SharedVaultInvites/DeleteInviteRequestParams.ts new file mode 100644 index 000000000..ede504353 --- /dev/null +++ b/packages/api/src/Domain/Request/SharedVaultInvites/DeleteInviteRequestParams.ts @@ -0,0 +1,4 @@ +export type DeleteInviteRequestParams = { + sharedVaultUuid: string + inviteUuid: string +} diff --git a/packages/api/src/Domain/Request/SharedVaultInvites/GetOutboundUserInvitesRequestParams.ts b/packages/api/src/Domain/Request/SharedVaultInvites/GetOutboundUserInvitesRequestParams.ts new file mode 100644 index 000000000..202488dff --- /dev/null +++ b/packages/api/src/Domain/Request/SharedVaultInvites/GetOutboundUserInvitesRequestParams.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/ban-types +export type GetOutboundUserInvitesRequestParams = {} diff --git a/packages/api/src/Domain/Request/SharedVaultInvites/GetSharedVaultInvitesRequestParams.ts b/packages/api/src/Domain/Request/SharedVaultInvites/GetSharedVaultInvitesRequestParams.ts new file mode 100644 index 000000000..fae9a93e6 --- /dev/null +++ b/packages/api/src/Domain/Request/SharedVaultInvites/GetSharedVaultInvitesRequestParams.ts @@ -0,0 +1,3 @@ +export type GetSharedVaultInvitesRequestParams = { + sharedVaultUuid: string +} diff --git a/packages/api/src/Domain/Request/SharedVaultInvites/GetUserInvitesRequestParams.ts b/packages/api/src/Domain/Request/SharedVaultInvites/GetUserInvitesRequestParams.ts new file mode 100644 index 000000000..1b5d00067 --- /dev/null +++ b/packages/api/src/Domain/Request/SharedVaultInvites/GetUserInvitesRequestParams.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/ban-types +export type GetUserInvitesRequestParams = {} diff --git a/packages/api/src/Domain/Request/SharedVaultInvites/UpdateSharedVaultInviteParams.ts b/packages/api/src/Domain/Request/SharedVaultInvites/UpdateSharedVaultInviteParams.ts new file mode 100644 index 000000000..3e701d0c6 --- /dev/null +++ b/packages/api/src/Domain/Request/SharedVaultInvites/UpdateSharedVaultInviteParams.ts @@ -0,0 +1,8 @@ +import { SharedVaultPermission } from '@standardnotes/responses' + +export type UpdateSharedVaultInviteParams = { + sharedVaultUuid: string + inviteUuid: string + encryptedMessage: string + permissions?: SharedVaultPermission +} diff --git a/packages/api/src/Domain/Request/SharedVaultUser/DeleteSharedVaultUserRequestParams.ts b/packages/api/src/Domain/Request/SharedVaultUser/DeleteSharedVaultUserRequestParams.ts new file mode 100644 index 000000000..231423431 --- /dev/null +++ b/packages/api/src/Domain/Request/SharedVaultUser/DeleteSharedVaultUserRequestParams.ts @@ -0,0 +1,4 @@ +export type DeleteSharedVaultUserRequestParams = { + sharedVaultUuid: string + userUuid: string +} diff --git a/packages/api/src/Domain/Request/SharedVaultUser/GetSharedVaultUsersRequestParams.ts b/packages/api/src/Domain/Request/SharedVaultUser/GetSharedVaultUsersRequestParams.ts new file mode 100644 index 000000000..3f14066d4 --- /dev/null +++ b/packages/api/src/Domain/Request/SharedVaultUser/GetSharedVaultUsersRequestParams.ts @@ -0,0 +1,3 @@ +export type GetSharedVaultUsersRequestParams = { + sharedVaultUuid: string +} diff --git a/packages/api/src/Domain/Request/Subscription/SubscriptionInviteCancelRequestParams.ts b/packages/api/src/Domain/Request/Subscription/SubscriptionInviteCancelRequestParams.ts index b68f1b420..28eb67d40 100644 --- a/packages/api/src/Domain/Request/Subscription/SubscriptionInviteCancelRequestParams.ts +++ b/packages/api/src/Domain/Request/Subscription/SubscriptionInviteCancelRequestParams.ts @@ -1,4 +1,4 @@ -import { ApiEndpointParam } from '../ApiEndpointParam' +import { ApiEndpointParam } from '@standardnotes/responses' import { ApiVersion } from '../../Api/ApiVersion' export type SubscriptionInviteCancelRequestParams = { diff --git a/packages/api/src/Domain/Request/Subscription/SubscriptionInviteDeclineRequestParams.ts b/packages/api/src/Domain/Request/Subscription/SubscriptionInviteDeclineRequestParams.ts index 67968c404..7a6d359e4 100644 --- a/packages/api/src/Domain/Request/Subscription/SubscriptionInviteDeclineRequestParams.ts +++ b/packages/api/src/Domain/Request/Subscription/SubscriptionInviteDeclineRequestParams.ts @@ -1,4 +1,4 @@ -import { ApiEndpointParam } from '../ApiEndpointParam' +import { ApiEndpointParam } from '@standardnotes/responses' import { ApiVersion } from '../../Api/ApiVersion' export type SubscriptionInviteDeclineRequestParams = { diff --git a/packages/api/src/Domain/Request/Subscription/SubscriptionInviteListRequestParams.ts b/packages/api/src/Domain/Request/Subscription/SubscriptionInviteListRequestParams.ts index bcf7fcc8d..58cfb77ae 100644 --- a/packages/api/src/Domain/Request/Subscription/SubscriptionInviteListRequestParams.ts +++ b/packages/api/src/Domain/Request/Subscription/SubscriptionInviteListRequestParams.ts @@ -1,4 +1,4 @@ -import { ApiEndpointParam } from '../ApiEndpointParam' +import { ApiEndpointParam } from '@standardnotes/responses' import { ApiVersion } from '../../Api/ApiVersion' export type SubscriptionInviteListRequestParams = { diff --git a/packages/api/src/Domain/Request/Subscription/SubscriptionInviteRequestParams.ts b/packages/api/src/Domain/Request/Subscription/SubscriptionInviteRequestParams.ts index 0263ec0df..c9d1dcbcd 100644 --- a/packages/api/src/Domain/Request/Subscription/SubscriptionInviteRequestParams.ts +++ b/packages/api/src/Domain/Request/Subscription/SubscriptionInviteRequestParams.ts @@ -1,4 +1,4 @@ -import { ApiEndpointParam } from '../ApiEndpointParam' +import { ApiEndpointParam } from '@standardnotes/responses' import { ApiVersion } from '../../Api/ApiVersion' export type SubscriptionInviteRequestParams = { diff --git a/packages/api/src/Domain/Request/User/UserRegistrationRequestParams.ts b/packages/api/src/Domain/Request/User/UserRegistrationRequestParams.ts index 126305d9f..3a6d20bca 100644 --- a/packages/api/src/Domain/Request/User/UserRegistrationRequestParams.ts +++ b/packages/api/src/Domain/Request/User/UserRegistrationRequestParams.ts @@ -1,13 +1,11 @@ import { AnyKeyParamsContent } from '@standardnotes/common' -import { ApiEndpointParam } from '../ApiEndpointParam' +import { ApiEndpointParam } from '@standardnotes/responses' import { ApiVersion } from '../../Api/ApiVersion' export type UserRegistrationRequestParams = AnyKeyParamsContent & { [ApiEndpointParam.ApiVersion]: ApiVersion.v0 + [additionalParam: string]: unknown password: string email: string ephemeral: boolean - [additionalParam: string]: unknown - pkcPublicKey?: string - pkcEncryptedPrivateKey?: string } diff --git a/packages/api/src/Domain/Request/User/UserUpdateRequestParams.ts b/packages/api/src/Domain/Request/User/UserUpdateRequestParams.ts new file mode 100644 index 000000000..8a9bf4423 --- /dev/null +++ b/packages/api/src/Domain/Request/User/UserUpdateRequestParams.ts @@ -0,0 +1,7 @@ +import { ApiEndpointParam } from '@standardnotes/responses' +import { ApiVersion } from '../../Api/ApiVersion' + +export type UserUpdateRequestParams = { + [ApiEndpointParam.ApiVersion]: ApiVersion.v0 + user_uuid: string +} diff --git a/packages/api/src/Domain/Request/index.ts b/packages/api/src/Domain/Request/index.ts index e6cd0f1a2..c18e5e6e5 100644 --- a/packages/api/src/Domain/Request/index.ts +++ b/packages/api/src/Domain/Request/index.ts @@ -1,4 +1,3 @@ -export * from './ApiEndpointParam' export * from './Authenticator/DeleteAuthenticatorRequestParams' export * from './Authenticator/GenerateAuthenticatorAuthenticationOptionsRequestParams' export * from './Authenticator/ListAuthenticatorsRequestParams' @@ -17,3 +16,4 @@ export * from './Subscription/SubscriptionInviteRequestParams' export * from './User/UserRegistrationRequestParams' export * from './UserRequest/UserRequestRequestParams' export * from './WebSocket/WebSocketConnectionTokenRequestParams' +export * from './SharedVault/SharedVaultMoveType' diff --git a/packages/api/src/Domain/Response/AsymmetricMessage/CreateAsymmetricMessageResponse.ts b/packages/api/src/Domain/Response/AsymmetricMessage/CreateAsymmetricMessageResponse.ts new file mode 100644 index 000000000..ae33abaac --- /dev/null +++ b/packages/api/src/Domain/Response/AsymmetricMessage/CreateAsymmetricMessageResponse.ts @@ -0,0 +1,5 @@ +import { AsymmetricMessageServerHash } from '@standardnotes/responses' + +export type CreateAsymmetricMessageResponse = { + message: AsymmetricMessageServerHash +} diff --git a/packages/api/src/Domain/Response/AsymmetricMessage/DeleteAsymmetricMessageResponse.ts b/packages/api/src/Domain/Response/AsymmetricMessage/DeleteAsymmetricMessageResponse.ts new file mode 100644 index 000000000..49b1ac3d5 --- /dev/null +++ b/packages/api/src/Domain/Response/AsymmetricMessage/DeleteAsymmetricMessageResponse.ts @@ -0,0 +1,3 @@ +export type DeleteAsymmetricMessageResponse = { + success: boolean +} diff --git a/packages/api/src/Domain/Response/AsymmetricMessage/GetOutboundAsymmetricMessagesResponse.ts b/packages/api/src/Domain/Response/AsymmetricMessage/GetOutboundAsymmetricMessagesResponse.ts new file mode 100644 index 000000000..963b3df9d --- /dev/null +++ b/packages/api/src/Domain/Response/AsymmetricMessage/GetOutboundAsymmetricMessagesResponse.ts @@ -0,0 +1,5 @@ +import { AsymmetricMessageServerHash } from '@standardnotes/responses' + +export type GetOutboundAsymmetricMessagesResponse = { + messages: AsymmetricMessageServerHash[] +} diff --git a/packages/api/src/Domain/Response/AsymmetricMessage/GetUserAsymmetricMessagesResponse.ts b/packages/api/src/Domain/Response/AsymmetricMessage/GetUserAsymmetricMessagesResponse.ts new file mode 100644 index 000000000..1ced414e6 --- /dev/null +++ b/packages/api/src/Domain/Response/AsymmetricMessage/GetUserAsymmetricMessagesResponse.ts @@ -0,0 +1,5 @@ +import { AsymmetricMessageServerHash } from '@standardnotes/responses' + +export type GetUserAsymmetricMessagesResponse = { + messages: AsymmetricMessageServerHash[] +} diff --git a/packages/api/src/Domain/Response/AsymmetricMessage/UpdateAsymmetricMessageResponse.ts b/packages/api/src/Domain/Response/AsymmetricMessage/UpdateAsymmetricMessageResponse.ts new file mode 100644 index 000000000..252e38c2f --- /dev/null +++ b/packages/api/src/Domain/Response/AsymmetricMessage/UpdateAsymmetricMessageResponse.ts @@ -0,0 +1,5 @@ +import { AsymmetricMessageServerHash } from '@standardnotes/responses' + +export type UpdateAsymmetricMessageResponse = { + message: AsymmetricMessageServerHash +} diff --git a/packages/api/src/Domain/Response/Revision/GetRevisionResponseBody.ts b/packages/api/src/Domain/Response/Revision/GetRevisionResponseBody.ts index 694aab441..2620faac8 100644 --- a/packages/api/src/Domain/Response/Revision/GetRevisionResponseBody.ts +++ b/packages/api/src/Domain/Response/Revision/GetRevisionResponseBody.ts @@ -7,6 +7,9 @@ export interface GetRevisionResponseBody { items_key_id: string | null enc_item_key: string | null auth_hash: string | null + user_uuid: string + key_system_identifier: string | null + shared_vault_uuid: string | null created_at: string updated_at: string } diff --git a/packages/api/src/Domain/Response/SharedVault/CreateSharedVaultResponse.ts b/packages/api/src/Domain/Response/SharedVault/CreateSharedVaultResponse.ts new file mode 100644 index 000000000..bf2176c90 --- /dev/null +++ b/packages/api/src/Domain/Response/SharedVault/CreateSharedVaultResponse.ts @@ -0,0 +1,6 @@ +import { SharedVaultUserServerHash, SharedVaultServerHash } from '@standardnotes/responses' + +export type CreateSharedVaultResponse = { + sharedVault: SharedVaultServerHash + sharedVaultUser: SharedVaultUserServerHash +} diff --git a/packages/api/src/Domain/Response/SharedVault/CreateSharedVaultValetTokenResponse.ts b/packages/api/src/Domain/Response/SharedVault/CreateSharedVaultValetTokenResponse.ts new file mode 100644 index 000000000..1784fd5b4 --- /dev/null +++ b/packages/api/src/Domain/Response/SharedVault/CreateSharedVaultValetTokenResponse.ts @@ -0,0 +1,3 @@ +export type CreateSharedVaultValetTokenResponse = { + valetToken: string +} diff --git a/packages/api/src/Domain/Response/SharedVault/GetSharedVaultsResponse.ts b/packages/api/src/Domain/Response/SharedVault/GetSharedVaultsResponse.ts new file mode 100644 index 000000000..8aa95ce6e --- /dev/null +++ b/packages/api/src/Domain/Response/SharedVault/GetSharedVaultsResponse.ts @@ -0,0 +1,5 @@ +import { SharedVaultServerHash } from '@standardnotes/responses' + +export type GetSharedVaultsResponse = { + sharedVaults: SharedVaultServerHash[] +} diff --git a/packages/api/src/Domain/Response/SharedVaultInvites/AcceptInviteResponse.ts b/packages/api/src/Domain/Response/SharedVaultInvites/AcceptInviteResponse.ts new file mode 100644 index 000000000..3c3f00dc0 --- /dev/null +++ b/packages/api/src/Domain/Response/SharedVaultInvites/AcceptInviteResponse.ts @@ -0,0 +1,3 @@ +export type AcceptInviteResponse = { + success: boolean +} diff --git a/packages/api/src/Domain/Response/SharedVaultInvites/CreateSharedVaultInviteResponse.ts b/packages/api/src/Domain/Response/SharedVaultInvites/CreateSharedVaultInviteResponse.ts new file mode 100644 index 000000000..51d145d27 --- /dev/null +++ b/packages/api/src/Domain/Response/SharedVaultInvites/CreateSharedVaultInviteResponse.ts @@ -0,0 +1,5 @@ +import { SharedVaultInviteServerHash } from '@standardnotes/responses' + +export type CreateSharedVaultInviteResponse = { + invite: SharedVaultInviteServerHash +} diff --git a/packages/api/src/Domain/Response/SharedVaultInvites/DeclineInviteResponse.ts b/packages/api/src/Domain/Response/SharedVaultInvites/DeclineInviteResponse.ts new file mode 100644 index 000000000..f46fdb675 --- /dev/null +++ b/packages/api/src/Domain/Response/SharedVaultInvites/DeclineInviteResponse.ts @@ -0,0 +1,3 @@ +export type DeclineInviteResponse = { + success: boolean +} diff --git a/packages/api/src/Domain/Response/SharedVaultInvites/DeleteAllSharedVaultInvitesResponse.ts b/packages/api/src/Domain/Response/SharedVaultInvites/DeleteAllSharedVaultInvitesResponse.ts new file mode 100644 index 000000000..0d1db7a4e --- /dev/null +++ b/packages/api/src/Domain/Response/SharedVaultInvites/DeleteAllSharedVaultInvitesResponse.ts @@ -0,0 +1,3 @@ +export type DeleteAllSharedVaultInvitesResponse = { + success: boolean +} diff --git a/packages/api/src/Domain/Response/SharedVaultInvites/DeleteInviteResponse.ts b/packages/api/src/Domain/Response/SharedVaultInvites/DeleteInviteResponse.ts new file mode 100644 index 000000000..4ae224eff --- /dev/null +++ b/packages/api/src/Domain/Response/SharedVaultInvites/DeleteInviteResponse.ts @@ -0,0 +1,3 @@ +export type DeleteInviteResponse = { + success: boolean +} diff --git a/packages/api/src/Domain/Response/SharedVaultInvites/GetOutboundUserInvitesResponse.ts b/packages/api/src/Domain/Response/SharedVaultInvites/GetOutboundUserInvitesResponse.ts new file mode 100644 index 000000000..0d4922d8e --- /dev/null +++ b/packages/api/src/Domain/Response/SharedVaultInvites/GetOutboundUserInvitesResponse.ts @@ -0,0 +1,5 @@ +import { SharedVaultInviteServerHash } from '@standardnotes/responses' + +export type GetOutboundUserInvitesResponse = { + invites: SharedVaultInviteServerHash[] +} diff --git a/packages/api/src/Domain/Response/SharedVaultInvites/GetSharedVaultInvitesResponse.ts b/packages/api/src/Domain/Response/SharedVaultInvites/GetSharedVaultInvitesResponse.ts new file mode 100644 index 000000000..1ca12153d --- /dev/null +++ b/packages/api/src/Domain/Response/SharedVaultInvites/GetSharedVaultInvitesResponse.ts @@ -0,0 +1,5 @@ +import { SharedVaultInviteServerHash } from '@standardnotes/responses' + +export type GetSharedVaultInvitesResponse = { + invites: SharedVaultInviteServerHash[] +} diff --git a/packages/api/src/Domain/Response/SharedVaultInvites/GetUserInvitesResponse.ts b/packages/api/src/Domain/Response/SharedVaultInvites/GetUserInvitesResponse.ts new file mode 100644 index 000000000..e5da886e4 --- /dev/null +++ b/packages/api/src/Domain/Response/SharedVaultInvites/GetUserInvitesResponse.ts @@ -0,0 +1,5 @@ +import { SharedVaultInviteServerHash } from '@standardnotes/responses' + +export type GetUserInvitesResponse = { + invites: SharedVaultInviteServerHash[] +} diff --git a/packages/api/src/Domain/Response/SharedVaultInvites/UpdateSharedVaultInviteResponse.ts b/packages/api/src/Domain/Response/SharedVaultInvites/UpdateSharedVaultInviteResponse.ts new file mode 100644 index 000000000..20498fac9 --- /dev/null +++ b/packages/api/src/Domain/Response/SharedVaultInvites/UpdateSharedVaultInviteResponse.ts @@ -0,0 +1,5 @@ +import { SharedVaultInviteServerHash } from '@standardnotes/responses' + +export type UpdateSharedVaultInviteResponse = { + invite: SharedVaultInviteServerHash +} diff --git a/packages/api/src/Domain/Response/SharedVaultUsers/DeleteSharedVaultUserResponse.ts b/packages/api/src/Domain/Response/SharedVaultUsers/DeleteSharedVaultUserResponse.ts new file mode 100644 index 000000000..ea9d71bdf --- /dev/null +++ b/packages/api/src/Domain/Response/SharedVaultUsers/DeleteSharedVaultUserResponse.ts @@ -0,0 +1,3 @@ +export type DeleteSharedVaultUserResponse = { + success: boolean +} diff --git a/packages/api/src/Domain/Response/SharedVaultUsers/GetSharedVaultUsersResponse.ts b/packages/api/src/Domain/Response/SharedVaultUsers/GetSharedVaultUsersResponse.ts new file mode 100644 index 000000000..7d159f0eb --- /dev/null +++ b/packages/api/src/Domain/Response/SharedVaultUsers/GetSharedVaultUsersResponse.ts @@ -0,0 +1,5 @@ +import { SharedVaultUserServerHash } from '@standardnotes/responses' + +export type GetSharedVaultUsersResponse = { + users: SharedVaultUserServerHash[] +} diff --git a/packages/api/src/Domain/Response/User/UserUpdateResponse.ts b/packages/api/src/Domain/Response/User/UserUpdateResponse.ts new file mode 100644 index 000000000..aba9e6010 --- /dev/null +++ b/packages/api/src/Domain/Response/User/UserUpdateResponse.ts @@ -0,0 +1,6 @@ +export type UserUpdateResponse = { + user: { + uuid: string + email: string + } +} diff --git a/packages/api/src/Domain/Response/index.ts b/packages/api/src/Domain/Response/index.ts index 4a183af46..05ef2d7cb 100644 --- a/packages/api/src/Domain/Response/index.ts +++ b/packages/api/src/Domain/Response/index.ts @@ -16,7 +16,9 @@ export * from './Subscription/SubscriptionInviteCancelResponseBody' export * from './Subscription/SubscriptionInviteDeclineResponseBody' export * from './Subscription/SubscriptionInviteListResponseBody' export * from './Subscription/SubscriptionInviteResponseBody' + export * from './User/UserDeletionResponseBody' export * from './User/UserRegistrationResponseBody' + export * from './UserRequest/UserRequestResponseBody' export * from './WebSocket/WebSocketConnectionTokenResponseBody' diff --git a/packages/api/src/Domain/Server/AsymmetricMessage/AsymmetricMessageServer.ts b/packages/api/src/Domain/Server/AsymmetricMessage/AsymmetricMessageServer.ts new file mode 100644 index 000000000..298e7ed46 --- /dev/null +++ b/packages/api/src/Domain/Server/AsymmetricMessage/AsymmetricMessageServer.ts @@ -0,0 +1,41 @@ +import { HttpResponse } from '@standardnotes/responses' +import { HttpServiceInterface } from '../../Http' +import { CreateAsymmetricMessageParams } from '../../Request/AsymmetricMessage/CreateAsymmetricMessageParams' +import { CreateAsymmetricMessageResponse } from '../../Response/AsymmetricMessage/CreateAsymmetricMessageResponse' +import { AsymmetricMessagesPaths } from './Paths' +import { GetUserAsymmetricMessagesResponse } from '../../Response/AsymmetricMessage/GetUserAsymmetricMessagesResponse' +import { AsymmetricMessageServerInterface } from './AsymmetricMessageServerInterface' +import { DeleteAsymmetricMessageRequestParams } from '../../Request/AsymmetricMessage/DeleteAsymmetricMessageRequestParams' +import { DeleteAsymmetricMessageResponse } from '../../Response/AsymmetricMessage/DeleteAsymmetricMessageResponse' + +export class AsymmetricMessageServer implements AsymmetricMessageServerInterface { + constructor(private httpService: HttpServiceInterface) {} + + createMessage(params: CreateAsymmetricMessageParams): Promise> { + return this.httpService.post(AsymmetricMessagesPaths.createMessage, { + recipient_uuid: params.recipientUuid, + encrypted_message: params.encryptedMessage, + replaceability_identifier: params.replaceabilityIdentifier, + }) + } + + getInboundUserMessages(): Promise> { + return this.httpService.get(AsymmetricMessagesPaths.getInboundUserMessages()) + } + + getOutboundUserMessages(): Promise> { + return this.httpService.get(AsymmetricMessagesPaths.getOutboundUserMessages()) + } + + getMessages(): Promise> { + return this.httpService.get(AsymmetricMessagesPaths.getMessages) + } + + deleteMessage(params: DeleteAsymmetricMessageRequestParams): Promise> { + return this.httpService.delete(AsymmetricMessagesPaths.deleteMessage(params.messageUuid)) + } + + deleteAllInboundMessages(): Promise> { + return this.httpService.delete(AsymmetricMessagesPaths.deleteAllInboundMessages) + } +} diff --git a/packages/api/src/Domain/Server/AsymmetricMessage/AsymmetricMessageServerInterface.ts b/packages/api/src/Domain/Server/AsymmetricMessage/AsymmetricMessageServerInterface.ts new file mode 100644 index 000000000..046eede55 --- /dev/null +++ b/packages/api/src/Domain/Server/AsymmetricMessage/AsymmetricMessageServerInterface.ts @@ -0,0 +1,17 @@ +import { HttpResponse } from '@standardnotes/responses' +import { CreateAsymmetricMessageParams } from '../../Request/AsymmetricMessage/CreateAsymmetricMessageParams' +import { CreateAsymmetricMessageResponse } from '../../Response/AsymmetricMessage/CreateAsymmetricMessageResponse' +import { GetUserAsymmetricMessagesResponse } from '../../Response/AsymmetricMessage/GetUserAsymmetricMessagesResponse' +import { DeleteAsymmetricMessageRequestParams } from '../../Request/AsymmetricMessage/DeleteAsymmetricMessageRequestParams' +import { DeleteAsymmetricMessageResponse } from '../../Response/AsymmetricMessage/DeleteAsymmetricMessageResponse' + +export interface AsymmetricMessageServerInterface { + createMessage(params: CreateAsymmetricMessageParams): Promise> + + getInboundUserMessages(): Promise> + getOutboundUserMessages(): Promise> + getMessages(): Promise> + + deleteMessage(params: DeleteAsymmetricMessageRequestParams): Promise> + deleteAllInboundMessages(): Promise> +} diff --git a/packages/api/src/Domain/Server/AsymmetricMessage/Paths.ts b/packages/api/src/Domain/Server/AsymmetricMessage/Paths.ts new file mode 100644 index 000000000..1f675fed8 --- /dev/null +++ b/packages/api/src/Domain/Server/AsymmetricMessage/Paths.ts @@ -0,0 +1,9 @@ +export const AsymmetricMessagesPaths = { + createMessage: '/v1/asymmetric-messages', + getMessages: '/v1/asymmetric-messages', + updateMessage: (messageUuid: string) => `/v1/asymmetric-messages/${messageUuid}`, + getInboundUserMessages: () => '/v1/asymmetric-messages', + getOutboundUserMessages: () => '/v1/asymmetric-messages/outbound', + deleteMessage: (messageUuid: string) => `/v1/asymmetric-messages/${messageUuid}`, + deleteAllInboundMessages: '/v1/asymmetric-messages/inbound', +} diff --git a/packages/api/src/Domain/Server/SharedVault/Paths.ts b/packages/api/src/Domain/Server/SharedVault/Paths.ts new file mode 100644 index 000000000..8c919f93f --- /dev/null +++ b/packages/api/src/Domain/Server/SharedVault/Paths.ts @@ -0,0 +1,7 @@ +export const SharedVaultsPaths = { + getSharedVaults: '/v1/shared-vaults', + createSharedVault: '/v1/shared-vaults', + deleteSharedVault: (sharedVaultUuid: string) => `/v1/shared-vaults/${sharedVaultUuid}`, + updateSharedVault: (sharedVaultUuid: string) => `/v1/shared-vaults/${sharedVaultUuid}`, + createSharedVaultFileValetToken: (sharedVaultUuid: string) => `/v1/shared-vaults/${sharedVaultUuid}/valet-tokens`, +} diff --git a/packages/api/src/Domain/Server/SharedVault/SharedVaultServer.ts b/packages/api/src/Domain/Server/SharedVault/SharedVaultServer.ts new file mode 100644 index 000000000..f8e719cb2 --- /dev/null +++ b/packages/api/src/Domain/Server/SharedVault/SharedVaultServer.ts @@ -0,0 +1,37 @@ +import { HttpResponse } from '@standardnotes/responses' +import { HttpServiceInterface } from '../../Http' +import { SharedVaultServerInterface } from './SharedVaultServerInterface' +import { SharedVaultsPaths } from './Paths' +import { CreateSharedVaultResponse } from '../../Response/SharedVault/CreateSharedVaultResponse' +import { GetSharedVaultsResponse } from '../../Response/SharedVault/GetSharedVaultsResponse' +import { CreateSharedVaultValetTokenResponse } from '../../Response/SharedVault/CreateSharedVaultValetTokenResponse' +import { CreateSharedVaultValetTokenParams } from '../../Request/SharedVault/CreateSharedVaultValetTokenParams' + +export class SharedVaultServer implements SharedVaultServerInterface { + constructor(private httpService: HttpServiceInterface) {} + + getSharedVaults(): Promise> { + return this.httpService.get(SharedVaultsPaths.getSharedVaults) + } + + createSharedVault(): Promise> { + return this.httpService.post(SharedVaultsPaths.createSharedVault) + } + + deleteSharedVault(params: { sharedVaultUuid: string }): Promise> { + return this.httpService.delete(SharedVaultsPaths.deleteSharedVault(params.sharedVaultUuid)) + } + + createSharedVaultFileValetToken( + params: CreateSharedVaultValetTokenParams, + ): Promise> { + return this.httpService.post(SharedVaultsPaths.createSharedVaultFileValetToken(params.sharedVaultUuid), { + file_uuid: params.fileUuid, + remote_identifier: params.remoteIdentifier, + operation: params.operation, + unencrypted_file_size: params.unencryptedFileSize, + move_operation_type: params.moveOperationType, + shared_vault_to_shared_vault_move_target_uuid: params.sharedVaultToSharedVaultMoveTargetUuid, + }) + } +} diff --git a/packages/api/src/Domain/Server/SharedVault/SharedVaultServerInterface.ts b/packages/api/src/Domain/Server/SharedVault/SharedVaultServerInterface.ts new file mode 100644 index 000000000..9059f7105 --- /dev/null +++ b/packages/api/src/Domain/Server/SharedVault/SharedVaultServerInterface.ts @@ -0,0 +1,17 @@ +import { HttpResponse } from '@standardnotes/responses' +import { CreateSharedVaultResponse } from '../../Response/SharedVault/CreateSharedVaultResponse' + +import { GetSharedVaultsResponse } from '../../Response/SharedVault/GetSharedVaultsResponse' +import { CreateSharedVaultValetTokenResponse } from '../../Response/SharedVault/CreateSharedVaultValetTokenResponse' +import { CreateSharedVaultValetTokenParams } from '../../Request/SharedVault/CreateSharedVaultValetTokenParams' + +export interface SharedVaultServerInterface { + getSharedVaults(): Promise> + + createSharedVault(): Promise> + deleteSharedVault(params: { sharedVaultUuid: string }): Promise> + + createSharedVaultFileValetToken( + params: CreateSharedVaultValetTokenParams, + ): Promise> +} diff --git a/packages/api/src/Domain/Server/SharedVaultInvites/Paths.ts b/packages/api/src/Domain/Server/SharedVaultInvites/Paths.ts new file mode 100644 index 000000000..3220745fd --- /dev/null +++ b/packages/api/src/Domain/Server/SharedVaultInvites/Paths.ts @@ -0,0 +1,16 @@ +export const SharedVaultInvitesPaths = { + createInvite: (sharedVaultUuid: string) => `/v1/shared-vaults/${sharedVaultUuid}/invites`, + updateInvite: (sharedVaultUuid: string, inviteUuid: string) => + `/v1/shared-vaults/${sharedVaultUuid}/invites/${inviteUuid}`, + acceptInvite: (sharedVaultUuid: string, inviteUuid: string) => + `/v1/shared-vaults/${sharedVaultUuid}/invites/${inviteUuid}/accept`, + declineInvite: (sharedVaultUuid: string, inviteUuid: string) => + `/v1/shared-vaults/${sharedVaultUuid}/invites/${inviteUuid}/decline`, + getInboundUserInvites: () => '/v1/shared-vaults/invites', + getOutboundUserInvites: () => '/v1/shared-vaults/invites/outbound', + getSharedVaultInvites: (sharedVaultUuid: string) => `/v1/shared-vaults/${sharedVaultUuid}/invites`, + deleteInvite: (sharedVaultUuid: string, inviteUuid: string) => + `/v1/shared-vaults/${sharedVaultUuid}/invites/${inviteUuid}`, + deleteAllSharedVaultInvites: (sharedVaultUuid: string) => `/v1/shared-vaults/${sharedVaultUuid}/invites`, + deleteAllInboundInvites: '/v1/shared-vaults/invites/inbound', +} diff --git a/packages/api/src/Domain/Server/SharedVaultInvites/SharedVaultInvitesServer.ts b/packages/api/src/Domain/Server/SharedVaultInvites/SharedVaultInvitesServer.ts new file mode 100644 index 000000000..85e13a01e --- /dev/null +++ b/packages/api/src/Domain/Server/SharedVaultInvites/SharedVaultInvitesServer.ts @@ -0,0 +1,75 @@ +import { HttpResponse } from '@standardnotes/responses' +import { HttpServiceInterface } from '../../Http' + +import { AcceptInviteRequestParams } from '../../Request/SharedVaultInvites/AcceptInviteRequestParams' +import { AcceptInviteResponse } from '../../Response/SharedVaultInvites/AcceptInviteResponse' +import { CreateSharedVaultInviteParams } from '../../Request/SharedVaultInvites/CreateSharedVaultInviteParams' +import { CreateSharedVaultInviteResponse } from '../../Response/SharedVaultInvites/CreateSharedVaultInviteResponse' +import { DeclineInviteRequestParams } from '../../Request/SharedVaultInvites/DeclineInviteRequestParams' +import { DeclineInviteResponse } from '../../Response/SharedVaultInvites/DeclineInviteResponse' +import { DeleteInviteRequestParams } from '../../Request/SharedVaultInvites/DeleteInviteRequestParams' +import { DeleteInviteResponse } from '../../Response/SharedVaultInvites/DeleteInviteResponse' +import { GetSharedVaultInvitesRequestParams } from '../../Request/SharedVaultInvites/GetSharedVaultInvitesRequestParams' +import { GetSharedVaultInvitesResponse } from '../../Response/SharedVaultInvites/GetSharedVaultInvitesResponse' +import { GetUserInvitesResponse } from '../../Response/SharedVaultInvites/GetUserInvitesResponse' +import { SharedVaultInvitesPaths } from './Paths' +import { SharedVaultInvitesServerInterface } from './SharedVaultInvitesServerInterface' +import { UpdateSharedVaultInviteParams } from '../../Request/SharedVaultInvites/UpdateSharedVaultInviteParams' +import { UpdateSharedVaultInviteResponse } from '../../Response/SharedVaultInvites/UpdateSharedVaultInviteResponse' +import { DeleteAllSharedVaultInvitesRequestParams } from '../../Request/SharedVaultInvites/DeleteAllSharedVaultInvitesRequestParams' +import { DeleteAllSharedVaultInvitesResponse } from '../../Response/SharedVaultInvites/DeleteAllSharedVaultInvitesResponse' + +export class SharedVaultInvitesServer implements SharedVaultInvitesServerInterface { + constructor(private httpService: HttpServiceInterface) {} + + createInvite(params: CreateSharedVaultInviteParams): Promise> { + return this.httpService.post(SharedVaultInvitesPaths.createInvite(params.sharedVaultUuid), { + recipient_uuid: params.recipientUuid, + encrypted_message: params.encryptedMessage, + permissions: params.permissions, + }) + } + + updateInvite(params: UpdateSharedVaultInviteParams): Promise> { + return this.httpService.patch(SharedVaultInvitesPaths.updateInvite(params.sharedVaultUuid, params.inviteUuid), { + encrypted_message: params.encryptedMessage, + permissions: params.permissions, + }) + } + + acceptInvite(params: AcceptInviteRequestParams): Promise> { + return this.httpService.post(SharedVaultInvitesPaths.acceptInvite(params.sharedVaultUuid, params.inviteUuid)) + } + + declineInvite(params: DeclineInviteRequestParams): Promise> { + return this.httpService.post(SharedVaultInvitesPaths.declineInvite(params.sharedVaultUuid, params.inviteUuid)) + } + + getInboundUserInvites(): Promise> { + return this.httpService.get(SharedVaultInvitesPaths.getInboundUserInvites()) + } + + getOutboundUserInvites(): Promise> { + return this.httpService.get(SharedVaultInvitesPaths.getOutboundUserInvites()) + } + + getSharedVaultInvites( + params: GetSharedVaultInvitesRequestParams, + ): Promise> { + return this.httpService.get(SharedVaultInvitesPaths.getSharedVaultInvites(params.sharedVaultUuid)) + } + + deleteInvite(params: DeleteInviteRequestParams): Promise> { + return this.httpService.delete(SharedVaultInvitesPaths.deleteInvite(params.sharedVaultUuid, params.inviteUuid)) + } + + deleteAllSharedVaultInvites( + params: DeleteAllSharedVaultInvitesRequestParams, + ): Promise> { + return this.httpService.delete(SharedVaultInvitesPaths.deleteAllSharedVaultInvites(params.sharedVaultUuid)) + } + + deleteAllInboundInvites(): Promise> { + return this.httpService.delete(SharedVaultInvitesPaths.deleteAllInboundInvites) + } +} diff --git a/packages/api/src/Domain/Server/SharedVaultInvites/SharedVaultInvitesServerInterface.ts b/packages/api/src/Domain/Server/SharedVaultInvites/SharedVaultInvitesServerInterface.ts new file mode 100644 index 000000000..f5fa2a498 --- /dev/null +++ b/packages/api/src/Domain/Server/SharedVaultInvites/SharedVaultInvitesServerInterface.ts @@ -0,0 +1,35 @@ +import { HttpResponse } from '@standardnotes/responses' +import { AcceptInviteRequestParams } from '../../Request/SharedVaultInvites/AcceptInviteRequestParams' +import { AcceptInviteResponse } from '../../Response/SharedVaultInvites/AcceptInviteResponse' +import { CreateSharedVaultInviteParams } from '../../Request/SharedVaultInvites/CreateSharedVaultInviteParams' +import { CreateSharedVaultInviteResponse } from '../../Response/SharedVaultInvites/CreateSharedVaultInviteResponse' +import { DeclineInviteRequestParams } from '../../Request/SharedVaultInvites/DeclineInviteRequestParams' +import { DeclineInviteResponse } from '../../Response/SharedVaultInvites/DeclineInviteResponse' +import { DeleteInviteRequestParams } from '../../Request/SharedVaultInvites/DeleteInviteRequestParams' +import { DeleteInviteResponse } from '../../Response/SharedVaultInvites/DeleteInviteResponse' +import { GetSharedVaultInvitesRequestParams } from '../../Request/SharedVaultInvites/GetSharedVaultInvitesRequestParams' +import { GetSharedVaultInvitesResponse } from '../../Response/SharedVaultInvites/GetSharedVaultInvitesResponse' +import { GetUserInvitesResponse } from '../../Response/SharedVaultInvites/GetUserInvitesResponse' +import { UpdateSharedVaultInviteParams } from '../../Request/SharedVaultInvites/UpdateSharedVaultInviteParams' +import { UpdateSharedVaultInviteResponse } from '../../Response/SharedVaultInvites/UpdateSharedVaultInviteResponse' +import { DeleteAllSharedVaultInvitesRequestParams } from '../../Request/SharedVaultInvites/DeleteAllSharedVaultInvitesRequestParams' +import { DeleteAllSharedVaultInvitesResponse } from '../../Response/SharedVaultInvites/DeleteAllSharedVaultInvitesResponse' + +export interface SharedVaultInvitesServerInterface { + createInvite(params: CreateSharedVaultInviteParams): Promise> + updateInvite(params: UpdateSharedVaultInviteParams): Promise> + acceptInvite(params: AcceptInviteRequestParams): Promise> + declineInvite(params: DeclineInviteRequestParams): Promise> + + getInboundUserInvites(): Promise> + getOutboundUserInvites(): Promise> + getSharedVaultInvites( + params: GetSharedVaultInvitesRequestParams, + ): Promise> + + deleteAllSharedVaultInvites( + params: DeleteAllSharedVaultInvitesRequestParams, + ): Promise> + deleteInvite(params: DeleteInviteRequestParams): Promise> + deleteAllInboundInvites(): Promise> +} diff --git a/packages/api/src/Domain/Server/SharedVaultUsers/Paths.ts b/packages/api/src/Domain/Server/SharedVaultUsers/Paths.ts new file mode 100644 index 000000000..ecab7f636 --- /dev/null +++ b/packages/api/src/Domain/Server/SharedVaultUsers/Paths.ts @@ -0,0 +1,5 @@ +export const SharedVaultUsersPaths = { + getSharedVaultUsers: (sharedVaultUuid: string) => `/v1/shared-vaults/${sharedVaultUuid}/users`, + deleteSharedVaultUser: (sharedVaultUuid: string, userUuid: string) => + `/v1/shared-vaults/${sharedVaultUuid}/users/${userUuid}`, +} diff --git a/packages/api/src/Domain/Server/SharedVaultUsers/SharedVaultUsersServer.ts b/packages/api/src/Domain/Server/SharedVaultUsers/SharedVaultUsersServer.ts new file mode 100644 index 000000000..299b2e3ba --- /dev/null +++ b/packages/api/src/Domain/Server/SharedVaultUsers/SharedVaultUsersServer.ts @@ -0,0 +1,22 @@ +import { HttpResponse } from '@standardnotes/responses' +import { HttpServiceInterface } from '../../Http' +import { GetSharedVaultUsersRequestParams } from '../../Request/SharedVaultUser/GetSharedVaultUsersRequestParams' +import { DeleteSharedVaultUserRequestParams } from '../../Request/SharedVaultUser/DeleteSharedVaultUserRequestParams' +import { DeleteSharedVaultUserResponse } from '../../Response/SharedVaultUsers/DeleteSharedVaultUserResponse' +import { SharedVaultUsersServerInterface } from './SharedVaultUsersServerInterface' +import { SharedVaultUsersPaths } from './Paths' +import { GetSharedVaultUsersResponse } from '../../Response/SharedVaultUsers/GetSharedVaultUsersResponse' + +export class SharedVaultUsersServer implements SharedVaultUsersServerInterface { + constructor(private httpService: HttpServiceInterface) {} + + getSharedVaultUsers(params: GetSharedVaultUsersRequestParams): Promise> { + return this.httpService.get(SharedVaultUsersPaths.getSharedVaultUsers(params.sharedVaultUuid)) + } + + deleteSharedVaultUser( + params: DeleteSharedVaultUserRequestParams, + ): Promise> { + return this.httpService.delete(SharedVaultUsersPaths.deleteSharedVaultUser(params.sharedVaultUuid, params.userUuid)) + } +} diff --git a/packages/api/src/Domain/Server/SharedVaultUsers/SharedVaultUsersServerInterface.ts b/packages/api/src/Domain/Server/SharedVaultUsers/SharedVaultUsersServerInterface.ts new file mode 100644 index 000000000..9cb87c423 --- /dev/null +++ b/packages/api/src/Domain/Server/SharedVaultUsers/SharedVaultUsersServerInterface.ts @@ -0,0 +1,13 @@ +import { HttpResponse } from '@standardnotes/responses' +import { GetSharedVaultUsersRequestParams } from '../../Request/SharedVaultUser/GetSharedVaultUsersRequestParams' +import { DeleteSharedVaultUserRequestParams } from '../../Request/SharedVaultUser/DeleteSharedVaultUserRequestParams' +import { DeleteSharedVaultUserResponse } from '../../Response/SharedVaultUsers/DeleteSharedVaultUserResponse' +import { GetSharedVaultUsersResponse } from '../../Response/SharedVaultUsers/GetSharedVaultUsersResponse' + +export interface SharedVaultUsersServerInterface { + getSharedVaultUsers(params: GetSharedVaultUsersRequestParams): Promise> + + deleteSharedVaultUser( + params: DeleteSharedVaultUserRequestParams, + ): Promise> +} diff --git a/packages/api/src/Domain/Server/User/Paths.ts b/packages/api/src/Domain/Server/User/Paths.ts index 9c9252775..55cc94ad1 100644 --- a/packages/api/src/Domain/Server/User/Paths.ts +++ b/packages/api/src/Domain/Server/User/Paths.ts @@ -1,5 +1,6 @@ const UserPaths = { register: '/v1/users', + updateAccount: (userUuid: string) => `/v1/users/${userUuid}`, deleteAccount: (userUuid: string) => `/v1/users/${userUuid}`, } diff --git a/packages/api/src/Domain/Server/User/UserServer.ts b/packages/api/src/Domain/Server/User/UserServer.ts index 7b5e22838..cafe32514 100644 --- a/packages/api/src/Domain/Server/User/UserServer.ts +++ b/packages/api/src/Domain/Server/User/UserServer.ts @@ -1,3 +1,4 @@ +import { UserUpdateResponse } from './../../Response/User/UserUpdateResponse' import { HttpServiceInterface } from '../../Http/HttpServiceInterface' import { UserDeletionRequestParams } from '../../Request/User/UserDeletionRequestParams' import { UserRegistrationRequestParams } from '../../Request/User/UserRegistrationRequestParams' @@ -6,6 +7,7 @@ import { UserDeletionResponseBody } from '../../Response/User/UserDeletionRespon import { UserRegistrationResponseBody } from '../../Response/User/UserRegistrationResponseBody' import { Paths } from './Paths' import { UserServerInterface } from './UserServerInterface' +import { UserUpdateRequestParams } from '../../Request/User/UserUpdateRequestParams' export class UserServer implements UserServerInterface { constructor(private httpService: HttpServiceInterface) {} @@ -17,4 +19,8 @@ export class UserServer implements UserServerInterface { async register(params: UserRegistrationRequestParams): Promise> { return this.httpService.post(Paths.v1.register, params) } + + async update(params: UserUpdateRequestParams): Promise> { + return this.httpService.patch(Paths.v1.updateAccount(params.user_uuid), params) + } } diff --git a/packages/api/src/Domain/Server/User/UserServerInterface.ts b/packages/api/src/Domain/Server/User/UserServerInterface.ts index b2016019b..1ce40c19d 100644 --- a/packages/api/src/Domain/Server/User/UserServerInterface.ts +++ b/packages/api/src/Domain/Server/User/UserServerInterface.ts @@ -3,8 +3,11 @@ import { UserDeletionRequestParams } from '../../Request/User/UserDeletionReques import { UserRegistrationRequestParams } from '../../Request/User/UserRegistrationRequestParams' import { UserDeletionResponseBody } from '../../Response/User/UserDeletionResponseBody' import { UserRegistrationResponseBody } from '../../Response/User/UserRegistrationResponseBody' +import { UserUpdateResponse } from '../../Response/User/UserUpdateResponse' +import { UserUpdateRequestParams } from '../../Request/User/UserUpdateRequestParams' export interface UserServerInterface { register(params: UserRegistrationRequestParams): Promise> deleteAccount(params: UserDeletionRequestParams): Promise> + update(params: UserUpdateRequestParams): Promise> } diff --git a/packages/api/src/Domain/Server/index.ts b/packages/api/src/Domain/Server/index.ts index f78be2fec..b9ed2d375 100644 --- a/packages/api/src/Domain/Server/index.ts +++ b/packages/api/src/Domain/Server/index.ts @@ -1,14 +1,32 @@ export * from './Auth/AuthServer' export * from './Auth/AuthServerInterface' + export * from './Authenticator/AuthenticatorServer' export * from './Authenticator/AuthenticatorServerInterface' + export * from './Revision/RevisionServer' export * from './Revision/RevisionServerInterface' + +export * from './AsymmetricMessage/AsymmetricMessageServer' +export * from './AsymmetricMessage/AsymmetricMessageServerInterface' + +export * from './SharedVault/SharedVaultServer' +export * from './SharedVault/SharedVaultServerInterface' + +export * from './SharedVaultUsers/SharedVaultUsersServer' +export * from './SharedVaultUsers/SharedVaultUsersServerInterface' + export * from './Subscription/SubscriptionServer' export * from './Subscription/SubscriptionServerInterface' + +export * from './SharedVaultInvites/SharedVaultInvitesServer' +export * from './SharedVaultInvites/SharedVaultInvitesServerInterface' + export * from './User/UserServer' export * from './User/UserServerInterface' + export * from './UserRequest/UserRequestServer' export * from './UserRequest/UserRequestServerInterface' + export * from './WebSocket/WebSocketServer' export * from './WebSocket/WebSocketServerInterface' diff --git a/packages/encryption/package.json b/packages/encryption/package.json index b898c53e3..b19c07e2c 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -5,22 +5,15 @@ "node": ">=16.0.0 <17.0.0" }, "description": "Payload encryption used in SNJS library", - "main": "dist/index.js", + "main": "./src/index.ts", + "private": true, "author": "Standard Notes", - "types": "dist/index.d.ts", - "files": [ - "dist" - ], "license": "AGPL-3.0-or-later", "scripts": { - "clean": "rm -fr dist", - "prestart": "yarn clean", - "start": "tsc -p tsconfig.json --watch", - "prebuild": "yarn clean", - "build": "tsc -p tsconfig.json", "lint": "eslint src --ext .ts", "format": "prettier --write src", - "test": "jest" + "test": "jest", + "build": "echo 'Empty build script required for yarn topological install'" }, "devDependencies": { "@standardnotes/config": "2.4.3", @@ -35,7 +28,7 @@ "typescript": "*" }, "dependencies": { - "@standardnotes/common": "^1.46.6", + "@standardnotes/common": "^1.48.3", "@standardnotes/models": "workspace:*", "@standardnotes/responses": "workspace:*", "@standardnotes/sncrypto-common": "workspace:*", diff --git a/packages/encryption/src/Domain/Algorithm.ts b/packages/encryption/src/Domain/Algorithm.ts index 9605320a9..45c320380 100644 --- a/packages/encryption/src/Domain/Algorithm.ts +++ b/packages/encryption/src/Domain/Algorithm.ts @@ -1,3 +1,5 @@ +import { SodiumConstant } from '@standardnotes/sncrypto-common' + export const V001Algorithm = Object.freeze({ SaltSeedLength: 128, /** @@ -41,11 +43,21 @@ export enum V004Algorithm { ArgonIterations = 5, ArgonMemLimit = 67108864, ArgonOutputKeyBytes = 64, + EncryptionKeyLength = 256, EncryptionNonceLength = 192, -} -export enum V005Algorithm { AsymmetricEncryptionNonceLength = 192, - SymmetricEncryptionNonceLength = 192, + + MasterKeyEncryptionKeyPairSubKeyNumber = 1, + MasterKeyEncryptionKeyPairSubKeyContext = 'sn-pkc-e', + MasterKeyEncryptionKeyPairSubKeyBytes = SodiumConstant.crypto_box_SEEDBYTES, + + MasterKeySigningKeyPairSubKeyNumber = 2, + MasterKeySigningKeyPairSubKeyContext = 'sn-pkc-s', + MasterKeySigningKeyPairSubKeyBytes = SodiumConstant.crypto_sign_SEEDBYTES, + + PayloadKeyHashingKeySubKeyNumber = 1, + PayloadKeyHashingKeySubKeyContext = 'sn-sym-h', + PayloadKeyHashingKeySubKeyBytes = SodiumConstant.crypto_generichash_KEYBYTES, } diff --git a/packages/encryption/src/Domain/Keys/ItemsKey/ItemsKey.ts b/packages/encryption/src/Domain/Keys/ItemsKey/ItemsKey.ts index 8201dbda9..383b76eb6 100644 --- a/packages/encryption/src/Domain/Keys/ItemsKey/ItemsKey.ts +++ b/packages/encryption/src/Domain/Keys/ItemsKey/ItemsKey.ts @@ -7,11 +7,10 @@ import { HistoryEntryInterface, ItemsKeyContent, ItemsKeyInterface, - RootKeyInterface, } from '@standardnotes/models' -export function isItemsKey(x: ItemsKeyInterface | RootKeyInterface): x is ItemsKeyInterface { - return x.content_type === ContentType.ItemsKey +export function isItemsKey(x: unknown): x is ItemsKeyInterface { + return (x as ItemsKeyInterface).content_type === ContentType.ItemsKey } /** diff --git a/packages/encryption/src/Domain/Keys/KeySystemItemsKey/KeySystemItemsKey.ts b/packages/encryption/src/Domain/Keys/KeySystemItemsKey/KeySystemItemsKey.ts new file mode 100644 index 000000000..9b52d4794 --- /dev/null +++ b/packages/encryption/src/Domain/Keys/KeySystemItemsKey/KeySystemItemsKey.ts @@ -0,0 +1,41 @@ +import { ContentType, ProtocolVersion } from '@standardnotes/common' +import { + ConflictStrategy, + DecryptedItem, + DecryptedItemInterface, + DecryptedPayloadInterface, + HistoryEntryInterface, + KeySystemItemsKeyContent, + KeySystemItemsKeyInterface, +} from '@standardnotes/models' + +export function isKeySystemItemsKey(x: unknown): x is KeySystemItemsKeyInterface { + return (x as KeySystemItemsKeyInterface).content_type === ContentType.KeySystemItemsKey +} + +/** + * A key used to encrypt other items. Items keys are synced and persisted. + */ +export class KeySystemItemsKey extends DecryptedItem implements KeySystemItemsKeyInterface { + creationTimestamp: number + keyVersion: ProtocolVersion + itemsKey: string + rootKeyToken: string + + constructor(payload: DecryptedPayloadInterface) { + super(payload) + + this.creationTimestamp = payload.content.creationTimestamp + this.keyVersion = payload.content.version + this.itemsKey = this.payload.content.itemsKey + this.rootKeyToken = this.payload.content.rootKeyToken + } + + /** Do not duplicate vault items keys. Always keep original */ + override strategyWhenConflictingWithItem( + _item: DecryptedItemInterface, + _previousRevision?: HistoryEntryInterface, + ): ConflictStrategy { + return ConflictStrategy.KeepBase + } +} diff --git a/packages/encryption/src/Domain/Keys/KeySystemItemsKey/KeySystemItemsKeyMutator.ts b/packages/encryption/src/Domain/Keys/KeySystemItemsKey/KeySystemItemsKeyMutator.ts new file mode 100644 index 000000000..5e8ee2793 --- /dev/null +++ b/packages/encryption/src/Domain/Keys/KeySystemItemsKey/KeySystemItemsKeyMutator.ts @@ -0,0 +1,3 @@ +import { DecryptedItemMutator, KeySystemItemsKeyContent } from '@standardnotes/models' + +export class KeySystemItemsKeyMutator extends DecryptedItemMutator {} diff --git a/packages/encryption/src/Domain/Keys/KeySystemItemsKey/Registration.ts b/packages/encryption/src/Domain/Keys/KeySystemItemsKey/Registration.ts new file mode 100644 index 000000000..721cecb64 --- /dev/null +++ b/packages/encryption/src/Domain/Keys/KeySystemItemsKey/Registration.ts @@ -0,0 +1,10 @@ +import { ContentType } from '@standardnotes/common' +import { DecryptedItemMutator, KeySystemItemsKeyContent, RegisterItemClass } from '@standardnotes/models' +import { KeySystemItemsKey } from './KeySystemItemsKey' +import { KeySystemItemsKeyMutator } from './KeySystemItemsKeyMutator' + +RegisterItemClass( + ContentType.KeySystemItemsKey, + KeySystemItemsKey, + KeySystemItemsKeyMutator as unknown as DecryptedItemMutator, +) diff --git a/packages/encryption/src/Domain/Keys/RootKey/Functions.ts b/packages/encryption/src/Domain/Keys/RootKey/Functions.ts index 793993b78..2cc0d30d5 100644 --- a/packages/encryption/src/Domain/Keys/RootKey/Functions.ts +++ b/packages/encryption/src/Domain/Keys/RootKey/Functions.ts @@ -5,11 +5,12 @@ import { PayloadTimestampDefaults, RootKeyContent, RootKeyContentSpecialized, + RootKeyInterface, } from '@standardnotes/models' import { UuidGenerator } from '@standardnotes/utils' import { SNRootKey } from './RootKey' -export function CreateNewRootKey(content: RootKeyContentSpecialized): SNRootKey { +export function CreateNewRootKey(content: RootKeyContentSpecialized): K { const uuid = UuidGenerator.GenerateUuid() const payload = new DecryptedPayload({ @@ -19,7 +20,7 @@ export function CreateNewRootKey(content: RootKeyContentSpecialized): SNRootKey ...PayloadTimestampDefaults(), }) - return new SNRootKey(payload) + return new SNRootKey(payload) as K } export function FillRootKeyContent(content: Partial): RootKeyContent { @@ -37,15 +38,3 @@ export function FillRootKeyContent(content: Partial): return FillItemContentSpecialized(content) } - -export function ContentTypeUsesRootKeyEncryption(contentType: ContentType): boolean { - return ( - contentType === ContentType.RootKey || - contentType === ContentType.ItemsKey || - contentType === ContentType.EncryptedStorage - ) -} - -export function ItemContentTypeUsesRootKeyEncryption(contentType: ContentType): boolean { - return contentType === ContentType.ItemsKey -} diff --git a/packages/encryption/src/Domain/Keys/RootKey/RootKey.ts b/packages/encryption/src/Domain/Keys/RootKey/RootKey.ts index 3b11dc825..03e77431a 100644 --- a/packages/encryption/src/Domain/Keys/RootKey/RootKey.ts +++ b/packages/encryption/src/Domain/Keys/RootKey/RootKey.ts @@ -7,7 +7,7 @@ import { RootKeyContentInStorage, RootKeyInterface, } from '@standardnotes/models' -import { timingSafeEqual } from '@standardnotes/sncrypto-common' +import { PkcKeyPair, timingSafeEqual } from '@standardnotes/sncrypto-common' import { SNRootKeyParams } from './RootKeyParams' /** @@ -47,6 +47,14 @@ export class SNRootKey extends DecryptedItem implements RootKeyI return this.content.serverPassword } + get encryptionKeyPair(): PkcKeyPair | undefined { + return this.content.encryptionKeyPair + } + + get signingKeyPair(): PkcKeyPair | undefined { + return this.content.signingKeyPair + } + /** 003 and below only. */ public get dataAuthenticationKey(): string | undefined { return this.content.dataAuthenticationKey @@ -84,6 +92,8 @@ export class SNRootKey extends DecryptedItem implements RootKeyI const values: NamespacedRootKeyInKeychain = { version: this.keyVersion, masterKey: this.masterKey, + encryptionKeyPair: this.encryptionKeyPair, + signingKeyPair: this.signingKeyPair, } if (this.dataAuthenticationKey) { diff --git a/packages/encryption/src/Domain/Operator/001/Operator001.ts b/packages/encryption/src/Domain/Operator/001/Operator001.ts index 878ea5884..93fe76c8d 100644 --- a/packages/encryption/src/Domain/Operator/001/Operator001.ts +++ b/packages/encryption/src/Domain/Operator/001/Operator001.ts @@ -8,8 +8,13 @@ import { ItemsKeyContent, ItemsKeyInterface, PayloadTimestampDefaults, + KeySystemItemsKeyInterface, + KeySystemIdentifier, + KeySystemRootKeyInterface, + RootKeyInterface, + KeySystemRootKeyParamsInterface, } from '@standardnotes/models' -import { PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common' import { firstHalfOfString, secondHalfOfString, splitString, UuidGenerator } from '@standardnotes/utils' import { V001Algorithm } from '../../Algorithm' import { isItemsKey } from '../../Keys/ItemsKey/ItemsKey' @@ -17,11 +22,16 @@ import { CreateNewRootKey } from '../../Keys/RootKey/Functions' import { Create001KeyParams } from '../../Keys/RootKey/KeyParamsFunctions' import { SNRootKey } from '../../Keys/RootKey/RootKey' import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams' -import { DecryptedParameters, EncryptedParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters' +import { EncryptedOutputParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters' +import { DecryptedParameters } from '../../Types/DecryptedParameters' import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData' import { LegacyAttachedData } from '../../Types/LegacyAttachedData' import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData' -import { AsynchronousOperator } from '../Operator' +import { OperatorInterface } from '../OperatorInterface/OperatorInterface' +import { PublicKeySet } from '../Types/PublicKeySet' +import { AsymmetricDecryptResult } from '../Types/AsymmetricDecryptResult' +import { AsymmetricSignatureVerificationDetachedResult } from '../Types/AsymmetricSignatureVerificationDetachedResult' +import { AsyncOperatorInterface } from '../OperatorInterface/AsyncOperatorInterface' const NO_IV = '00000000000000000000000000000000' @@ -29,7 +39,7 @@ const NO_IV = '00000000000000000000000000000000' * @deprecated * A legacy operator no longer used to generate new accounts */ -export class SNProtocolOperator001 implements AsynchronousOperator { +export class SNProtocolOperator001 implements OperatorInterface, AsyncOperatorInterface { protected readonly crypto: PureCryptoInterface constructor(crypto: PureCryptoInterface) { @@ -68,11 +78,11 @@ export class SNProtocolOperator001 implements AsynchronousOperator { return CreateDecryptedItemFromPayload(payload) } - public async createRootKey( + public async createRootKey( identifier: string, password: string, origination: KeyParamsOrigination, - ): Promise { + ): Promise { const pwCost = V001Algorithm.PbkdfMinCost as number const pwNonce = this.crypto.generateRandomKey(V001Algorithm.SaltSeedLength) const pwSalt = await this.crypto.unsafeSha1(identifier + 'SN' + pwNonce) @@ -90,13 +100,13 @@ export class SNProtocolOperator001 implements AsynchronousOperator { return this.deriveKey(password, keyParams) } - public getPayloadAuthenticatedData( - _encrypted: EncryptedParameters, + public getPayloadAuthenticatedDataForExternalUse( + _encrypted: EncryptedOutputParameters, ): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined { return undefined } - public async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise { + public async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise { return this.deriveKey(password, keyParams) } @@ -111,7 +121,7 @@ export class SNProtocolOperator001 implements AsynchronousOperator { public async generateEncryptedParametersAsync( payload: DecryptedPayloadInterface, key: ItemsKeyInterface | SNRootKey, - ): Promise { + ): Promise { /** * Generate new item key that is double the key size. * Will be split to create encryption key and authentication key. @@ -132,16 +142,19 @@ export class SNProtocolOperator001 implements AsynchronousOperator { return { uuid: payload.uuid, + content_type: payload.content_type, items_key_id: isItemsKey(key) ? key.uuid : undefined, content: ciphertext, enc_item_key: encItemKey, auth_hash: authHash, version: this.version, + key_system_identifier: payload.key_system_identifier, + shared_vault_uuid: payload.shared_vault_uuid, } } public async generateDecryptedParametersAsync( - encrypted: EncryptedParameters, + encrypted: EncryptedOutputParameters, key: ItemsKeyInterface | SNRootKey, ): Promise | ErrorDecryptingParameters> { if (!encrypted.enc_item_key) { @@ -178,6 +191,7 @@ export class SNProtocolOperator001 implements AsynchronousOperator { return { uuid: encrypted.uuid, content: JSON.parse(content), + signatureData: { required: false, contentHash: '' }, } } } @@ -191,7 +205,7 @@ export class SNProtocolOperator001 implements AsynchronousOperator { } } - protected async deriveKey(password: string, keyParams: SNRootKeyParams): Promise { + protected async deriveKey(password: string, keyParams: SNRootKeyParams): Promise { const derivedKey = await this.crypto.pbkdf2( password, keyParams.content001.pw_salt, @@ -205,11 +219,63 @@ export class SNProtocolOperator001 implements AsynchronousOperator { const partitions = splitString(derivedKey, 2) - return CreateNewRootKey({ + return CreateNewRootKey({ serverPassword: partitions[0], masterKey: partitions[1], version: ProtocolVersion.V001, keyParams: keyParams.getPortableValue(), }) } + + createRandomizedKeySystemRootKey(_dto: { systemIdentifier: string }): KeySystemRootKeyInterface { + throw new Error('Method not implemented.') + } + + createUserInputtedKeySystemRootKey(_dto: { + systemIdentifier: string + systemName: string + userInputtedPassword: string + }): KeySystemRootKeyInterface { + throw new Error('Method not implemented.') + } + + deriveUserInputtedKeySystemRootKey(_dto: { + keyParams: KeySystemRootKeyParamsInterface + userInputtedPassword: string + }): KeySystemRootKeyInterface { + throw new Error('Method not implemented.') + } + + createKeySystemItemsKey( + _uuid: string, + _keySystemIdentifier: KeySystemIdentifier, + _sharedVaultUuid: string | undefined, + ): KeySystemItemsKeyInterface { + throw new Error('Method not implemented.') + } + + versionForAsymmetricallyEncryptedString(_encryptedString: string): ProtocolVersion { + throw new Error('Method not implemented.') + } + + asymmetricEncrypt(_dto: { + stringToEncrypt: string + senderKeyPair: PkcKeyPair + senderSigningKeyPair: PkcKeyPair + recipientPublicKey: string + }): string { + throw new Error('Method not implemented.') + } + + asymmetricDecrypt(_dto: { stringToDecrypt: string; recipientSecretKey: string }): AsymmetricDecryptResult | null { + throw new Error('Method not implemented.') + } + + asymmetricSignatureVerifyDetached(_encryptedString: string): AsymmetricSignatureVerificationDetachedResult { + throw new Error('Method not implemented.') + } + + getSenderPublicKeySetFromAsymmetricallyEncryptedString(_string: string): PublicKeySet { + throw new Error('Method not implemented.') + } } diff --git a/packages/encryption/src/Domain/Operator/002/Operator002.ts b/packages/encryption/src/Domain/Operator/002/Operator002.ts index 08e74f5ad..5e7822f53 100644 --- a/packages/encryption/src/Domain/Operator/002/Operator002.ts +++ b/packages/encryption/src/Domain/Operator/002/Operator002.ts @@ -9,7 +9,8 @@ import { CreateNewRootKey } from '../../Keys/RootKey/Functions' import { Create002KeyParams } from '../../Keys/RootKey/KeyParamsFunctions' import { SNRootKey } from '../../Keys/RootKey/RootKey' import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams' -import { DecryptedParameters, EncryptedParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters' +import { EncryptedOutputParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters' +import { DecryptedParameters } from '../../Types/DecryptedParameters' import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData' import { LegacyAttachedData } from '../../Types/LegacyAttachedData' import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData' @@ -50,11 +51,11 @@ export class SNProtocolOperator002 extends SNProtocolOperator001 { return Models.CreateDecryptedItemFromPayload(payload) } - public override async createRootKey( + public override async createRootKey( identifier: string, password: string, origination: Common.KeyParamsOrigination, - ): Promise { + ): Promise { const pwCost = Utils.lastElement(V002Algorithm.PbkdfCostsUsed) as number const pwNonce = this.crypto.generateRandomKey(V002Algorithm.SaltSeedLength) const pwSalt = await this.crypto.unsafeSha1(identifier + ':' + pwNonce) @@ -77,7 +78,10 @@ export class SNProtocolOperator002 extends SNProtocolOperator001 { * may have had costs of 5000, and others of 101000. Therefore, when computing * the root key, we must use the value returned by the server. */ - public override async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise { + public override async computeRootKey( + password: string, + keyParams: SNRootKeyParams, + ): Promise { return this.deriveKey(password, keyParams) } @@ -141,8 +145,8 @@ export class SNProtocolOperator002 extends SNProtocolOperator001 { return this.decryptString002(contentCiphertext, encryptionKey, iv) } - public override getPayloadAuthenticatedData( - encrypted: EncryptedParameters, + public override getPayloadAuthenticatedDataForExternalUse( + encrypted: EncryptedOutputParameters, ): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined { const itemKeyComponents = this.encryptionComponentsFromString002(encrypted.enc_item_key) const authenticatedData = itemKeyComponents.keyParams @@ -161,7 +165,7 @@ export class SNProtocolOperator002 extends SNProtocolOperator001 { public override async generateEncryptedParametersAsync( payload: Models.DecryptedPayloadInterface, key: Models.ItemsKeyInterface | SNRootKey, - ): Promise { + ): Promise { /** * Generate new item key that is double the key size. * Will be split to create encryption key and authentication key. @@ -189,15 +193,18 @@ export class SNProtocolOperator002 extends SNProtocolOperator001 { return { uuid: payload.uuid, + content_type: payload.content_type, items_key_id: isItemsKey(key) ? key.uuid : undefined, content: ciphertext, enc_item_key: encItemKey, version: this.version, + key_system_identifier: payload.key_system_identifier, + shared_vault_uuid: payload.shared_vault_uuid, } } public override async generateDecryptedParametersAsync( - encrypted: EncryptedParameters, + encrypted: EncryptedOutputParameters, key: Models.ItemsKeyInterface | SNRootKey, ): Promise | ErrorDecryptingParameters> { if (!encrypted.enc_item_key) { @@ -252,11 +259,15 @@ export class SNProtocolOperator002 extends SNProtocolOperator001 { return { uuid: encrypted.uuid, content: JSON.parse(content), + signatureData: { required: false, contentHash: '' }, } } } - protected override async deriveKey(password: string, keyParams: SNRootKeyParams): Promise { + protected override async deriveKey( + password: string, + keyParams: SNRootKeyParams, + ): Promise { const derivedKey = await this.crypto.pbkdf2( password, keyParams.content002.pw_salt, @@ -270,7 +281,7 @@ export class SNProtocolOperator002 extends SNProtocolOperator001 { const partitions = Utils.splitString(derivedKey, 3) - return CreateNewRootKey({ + return CreateNewRootKey({ serverPassword: partitions[0], masterKey: partitions[1], dataAuthenticationKey: partitions[2], diff --git a/packages/encryption/src/Domain/Operator/003/Operator003.ts b/packages/encryption/src/Domain/Operator/003/Operator003.ts index d5e47a8f0..9273844fe 100644 --- a/packages/encryption/src/Domain/Operator/003/Operator003.ts +++ b/packages/encryption/src/Domain/Operator/003/Operator003.ts @@ -6,12 +6,12 @@ import { ItemsKeyContent, ItemsKeyInterface, PayloadTimestampDefaults, + RootKeyInterface, } from '@standardnotes/models' import { splitString, UuidGenerator } from '@standardnotes/utils' import { V003Algorithm } from '../../Algorithm' import { CreateNewRootKey } from '../../Keys/RootKey/Functions' import { Create003KeyParams } from '../../Keys/RootKey/KeyParamsFunctions' -import { SNRootKey } from '../../Keys/RootKey/RootKey' import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams' import { SNProtocolOperator002 } from '../002/Operator002' @@ -53,11 +53,17 @@ export class SNProtocolOperator003 extends SNProtocolOperator002 { return CreateDecryptedItemFromPayload(payload) } - public override async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise { + public override async computeRootKey( + password: string, + keyParams: SNRootKeyParams, + ): Promise { return this.deriveKey(password, keyParams) } - protected override async deriveKey(password: string, keyParams: SNRootKeyParams): Promise { + protected override async deriveKey( + password: string, + keyParams: SNRootKeyParams, + ): Promise { const salt = await this.generateSalt( keyParams.content003.identifier, ProtocolVersion.V003, @@ -78,7 +84,7 @@ export class SNProtocolOperator003 extends SNProtocolOperator002 { const partitions = splitString(derivedKey, 3) - return CreateNewRootKey({ + return CreateNewRootKey({ serverPassword: partitions[0], masterKey: partitions[1], dataAuthenticationKey: partitions[2], @@ -87,11 +93,11 @@ export class SNProtocolOperator003 extends SNProtocolOperator002 { }) } - public override async createRootKey( + public override async createRootKey( identifier: string, password: string, origination: KeyParamsOrigination, - ): Promise { + ): Promise { const version = ProtocolVersion.V003 const pwNonce = this.crypto.generateRandomKey(V003Algorithm.SaltSeedLength) const keyParams = Create003KeyParams({ diff --git a/packages/encryption/src/Domain/Operator/004/MockedCrypto.ts b/packages/encryption/src/Domain/Operator/004/MockedCrypto.ts new file mode 100644 index 000000000..c6a012a3b --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/MockedCrypto.ts @@ -0,0 +1,87 @@ +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' + +export function getMockedCrypto(): PureCryptoInterface { + const crypto = {} as jest.Mocked + + const mockGenerateKeyPair = (seed: string) => { + const publicKey = `public-key-${seed}` + const privateKey = `private-key-${seed}` + + return { + publicKey: `${publicKey}:${privateKey}`, + privateKey: `${privateKey}:${publicKey}`, + } + } + + const replaceColonsToAvoidJSONConflicts = (text: string) => { + return text.replace(/:/g, '|') + } + + const undoReplaceColonsToAvoidJSONConflicts = (text: string) => { + return text.replace(/\|/g, ':') + } + + crypto.base64Encode = jest.fn().mockImplementation((text: string) => { + return `base64-${replaceColonsToAvoidJSONConflicts(text)}` + }) + + crypto.base64Decode = jest.fn().mockImplementation((text: string) => { + const decodedText = text.split('base64-')[1] + return undoReplaceColonsToAvoidJSONConflicts(decodedText) + }) + + crypto.xchacha20Encrypt = jest.fn().mockImplementation((text: string) => { + return `${replaceColonsToAvoidJSONConflicts(text)}` + }) + + crypto.xchacha20Decrypt = jest.fn().mockImplementation((text: string) => { + return undoReplaceColonsToAvoidJSONConflicts(text.split('')[1]) + }) + + crypto.generateRandomKey = jest.fn().mockImplementation(() => { + return 'random-string' + }) + + crypto.sodiumCryptoBoxEasyEncrypt = jest.fn().mockImplementation((text: string) => { + return `${replaceColonsToAvoidJSONConflicts(text)}` + }) + + crypto.sodiumCryptoBoxEasyDecrypt = jest.fn().mockImplementation((text: string) => { + return undoReplaceColonsToAvoidJSONConflicts(text.split('')[1]) + }) + + crypto.sodiumCryptoBoxSeedKeypair = jest.fn().mockImplementation((seed: string) => { + return mockGenerateKeyPair(seed) + }) + + crypto.sodiumCryptoKdfDeriveFromKey = jest + .fn() + .mockImplementation((key: string, subkeyNumber: number, subkeyLength: number, context: string) => { + return `subkey-${key}-${subkeyNumber}-${subkeyLength}-${context}` + }) + + crypto.sodiumCryptoSign = jest.fn().mockImplementation((message: string, privateKey: string) => { + const signature = `signature:m=${message}:pk=${privateKey}` + return signature + }) + + crypto.sodiumCryptoSignSeedKeypair = jest.fn().mockImplementation((seed: string) => { + return mockGenerateKeyPair(seed) + }) + + crypto.sodiumCryptoSignVerify = jest + .fn() + .mockImplementation((message: string, signature: string, publicKey: string) => { + const keyComponents = publicKey.split(':') + const privateKeyComponent = keyComponents[1] + const privateKey = `${privateKeyComponent}:${keyComponents[0]}` + const computedSignature = crypto.sodiumCryptoSign(message, privateKey) + return computedSignature === signature + }) + + crypto.sodiumCryptoGenericHash = jest.fn().mockImplementation((message: string, key: string) => { + return `hash-${message}-${key}` + }) + + return crypto +} diff --git a/packages/encryption/src/Domain/Operator/004/Operator004.spec.ts b/packages/encryption/src/Domain/Operator/004/Operator004.spec.ts index b7a717c5e..1118292bf 100644 --- a/packages/encryption/src/Domain/Operator/004/Operator004.spec.ts +++ b/packages/encryption/src/Domain/Operator/004/Operator004.spec.ts @@ -1,69 +1,34 @@ import { ContentType, ProtocolVersion } from '@standardnotes/common' import { DecryptedPayload, ItemContent, ItemsKeyContent, PayloadTimestampDefaults } from '@standardnotes/models' -import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { SNItemsKey } from '../../Keys/ItemsKey/ItemsKey' -import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData' import { SNProtocolOperator004 } from './Operator004' - -const b64 = (text: string): string => { - return Buffer.from(text).toString('base64') -} +import { getMockedCrypto } from './MockedCrypto' +import { deconstructEncryptedPayloadString } from './V004AlgorithmHelpers' describe('operator 004', () => { - let crypto: PureCryptoInterface + const crypto = getMockedCrypto() + let operator: SNProtocolOperator004 beforeEach(() => { - crypto = {} as jest.Mocked - crypto.base64Encode = jest.fn().mockImplementation((text: string) => { - return b64(text) - }) - crypto.base64Decode = jest.fn().mockImplementation((text: string) => { - return Buffer.from(text, 'base64').toString('ascii') - }) - crypto.xchacha20Encrypt = jest.fn().mockImplementation((text: string) => { - return `${text}` - }) - crypto.xchacha20Decrypt = jest.fn().mockImplementation((text: string) => { - return text.split('')[1] - }) - crypto.generateRandomKey = jest.fn().mockImplementation(() => { - return 'random-string' - }) - operator = new SNProtocolOperator004(crypto) }) - it('should generateEncryptedProtocolString', () => { - const aad: ItemAuthenticatedData = { - u: '123', - v: ProtocolVersion.V004, - } - - const nonce = 'noncy' - const plaintext = 'foo' - - operator.generateEncryptionNonce = jest.fn().mockReturnValue(nonce) - - const result = operator.generateEncryptedProtocolString(plaintext, 'secret', aad) - - expect(result).toEqual(`004:${nonce}:${plaintext}:${b64(JSON.stringify(aad))}`) - }) - it('should deconstructEncryptedPayloadString', () => { const string = '004:noncy:foo:eyJ1IjoiMTIzIiwidiI6IjAwNCJ9' - const result = operator.deconstructEncryptedPayloadString(string) + const result = deconstructEncryptedPayloadString(string) expect(result).toEqual({ version: '004', nonce: 'noncy', ciphertext: 'foo', authenticatedData: 'eyJ1IjoiMTIzIiwidiI6IjAwNCJ9', + additionalData: 'e30=', }) }) - it('should generateEncryptedParametersSync', () => { + it('should generateEncryptedParameters', () => { const payload = { uuid: '123', content_type: ContentType.Note, @@ -83,13 +48,16 @@ describe('operator 004', () => { }), ) - const result = operator.generateEncryptedParametersSync(payload, key) + const result = operator.generateEncryptedParameters(payload, key) expect(result).toEqual({ uuid: '123', items_key_id: 'key-456', - content: '004:random-string:{"foo":"bar"}:eyJ1IjoiMTIzIiwidiI6IjAwNCJ9', - enc_item_key: '004:random-string:random-string:eyJ1IjoiMTIzIiwidiI6IjAwNCJ9', + key_system_identifier: undefined, + shared_vault_uuid: undefined, + content: '004:random-string:{"foo"|"bar"}:base64-{"u"|"123","v"|"004"}:base64-{}', + content_type: ContentType.Note, + enc_item_key: '004:random-string:random-string:base64-{"u"|"123","v"|"004"}:base64-{}', version: '004', }) }) diff --git a/packages/encryption/src/Domain/Operator/004/Operator004.ts b/packages/encryption/src/Domain/Operator/004/Operator004.ts index aa2a0975d..5fceb1121 100644 --- a/packages/encryption/src/Domain/Operator/004/Operator004.ts +++ b/packages/encryption/src/Domain/Operator/004/Operator004.ts @@ -1,44 +1,56 @@ -import { ContentType, KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common' -import * as Models from '@standardnotes/models' import { CreateDecryptedItemFromPayload, - FillItemContent, ItemContent, - ItemsKeyContent, ItemsKeyInterface, PayloadTimestampDefaults, + DecryptedPayload, + DecryptedPayloadInterface, + KeySystemItemsKeyInterface, + KeySystemRootKeyInterface, + FillItemContentSpecialized, + ItemsKeyContentSpecialized, + KeySystemIdentifier, + RootKeyInterface, + KeySystemRootKeyParamsInterface, } from '@standardnotes/models' -import { PureCryptoInterface } from '@standardnotes/sncrypto-common' -import * as Utils from '@standardnotes/utils' +import { ContentType, KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common' +import { HexString, PkcKeyPair, PureCryptoInterface, Utf8String } from '@standardnotes/sncrypto-common' import { V004Algorithm } from '../../Algorithm' -import { isItemsKey } from '../../Keys/ItemsKey/ItemsKey' -import { ContentTypeUsesRootKeyEncryption, CreateNewRootKey } from '../../Keys/RootKey/Functions' -import { Create004KeyParams } from '../../Keys/RootKey/KeyParamsFunctions' -import { SNRootKey } from '../../Keys/RootKey/RootKey' import { SNRootKeyParams } from '../../Keys/RootKey/RootKeyParams' -import { DecryptedParameters, EncryptedParameters, ErrorDecryptingParameters } from '../../Types/EncryptedParameters' +import { + EncryptedInputParameters, + EncryptedOutputParameters, + ErrorDecryptingParameters, +} from '../../Types/EncryptedParameters' +import { DecryptedParameters } from '../../Types/DecryptedParameters' import { ItemAuthenticatedData } from '../../Types/ItemAuthenticatedData' import { LegacyAttachedData } from '../../Types/LegacyAttachedData' import { RootKeyEncryptedAuthenticatedData } from '../../Types/RootKeyEncryptedAuthenticatedData' -import { SynchronousOperator } from '../Operator' +import { OperatorInterface } from '../OperatorInterface/OperatorInterface' +import { AsymmetricallyEncryptedString } from '../Types/Types' +import { AsymmetricItemAdditionalData } from '../../Types/EncryptionAdditionalData' +import { V004AsymmetricStringComponents } from './V004AlgorithmTypes' +import { AsymmetricEncryptUseCase } from './UseCase/Asymmetric/AsymmetricEncrypt' +import { ParseConsistentBase64JsonPayloadUseCase } from './UseCase/Utils/ParseConsistentBase64JsonPayload' +import { AsymmetricDecryptUseCase } from './UseCase/Asymmetric/AsymmetricDecrypt' +import { GenerateDecryptedParametersUseCase } from './UseCase/Symmetric/GenerateDecryptedParameters' +import { GenerateEncryptedParametersUseCase } from './UseCase/Symmetric/GenerateEncryptedParameters' +import { DeriveRootKeyUseCase } from './UseCase/RootKey/DeriveRootKey' +import { GetPayloadAuthenticatedDataDetachedUseCase } from './UseCase/Symmetric/GetPayloadAuthenticatedDataDetached' +import { CreateRootKeyUseCase } from './UseCase/RootKey/CreateRootKey' +import { UuidGenerator } from '@standardnotes/utils' +import { CreateKeySystemItemsKeyUseCase } from './UseCase/KeySystem/CreateKeySystemItemsKey' +import { AsymmetricDecryptResult } from '../Types/AsymmetricDecryptResult' +import { PublicKeySet } from '../Types/PublicKeySet' +import { CreateRandomKeySystemRootKey } from './UseCase/KeySystem/CreateRandomKeySystemRootKey' +import { CreateUserInputKeySystemRootKey } from './UseCase/KeySystem/CreateUserInputKeySystemRootKey' +import { AsymmetricSignatureVerificationDetachedResult } from '../Types/AsymmetricSignatureVerificationDetachedResult' +import { AsymmetricSignatureVerificationDetachedUseCase } from './UseCase/Asymmetric/AsymmetricSignatureVerificationDetached' +import { DeriveKeySystemRootKeyUseCase } from './UseCase/KeySystem/DeriveKeySystemRootKey' +import { SyncOperatorInterface } from '../OperatorInterface/SyncOperatorInterface' -type V004StringComponents = [version: string, nonce: string, ciphertext: string, authenticatedData: string] - -type V004Components = { - version: V004StringComponents[0] - nonce: V004StringComponents[1] - ciphertext: V004StringComponents[2] - authenticatedData: V004StringComponents[3] -} - -const PARTITION_CHARACTER = ':' - -export class SNProtocolOperator004 implements SynchronousOperator { - protected readonly crypto: PureCryptoInterface - - constructor(crypto: PureCryptoInterface) { - this.crypto = crypto - } +export class SNProtocolOperator004 implements OperatorInterface, SyncOperatorInterface { + constructor(protected readonly crypto: PureCryptoInterface) {} public getEncryptionDisplayName(): string { return 'XChaCha20-Poly1305' @@ -50,7 +62,7 @@ export class SNProtocolOperator004 implements SynchronousOperator { private generateNewItemsKeyContent() { const itemsKey = this.crypto.generateRandomKey(V004Algorithm.EncryptionKeyLength) - const response = FillItemContent({ + const response = FillItemContentSpecialized({ itemsKey: itemsKey, version: ProtocolVersion.V004, }) @@ -62,260 +74,130 @@ export class SNProtocolOperator004 implements SynchronousOperator { * The consumer must save/sync this item. */ public createItemsKey(): ItemsKeyInterface { - const payload = new Models.DecryptedPayload({ - uuid: Utils.UuidGenerator.GenerateUuid(), + const payload = new DecryptedPayload({ + uuid: UuidGenerator.GenerateUuid(), content_type: ContentType.ItemsKey, content: this.generateNewItemsKeyContent(), + key_system_identifier: undefined, + shared_vault_uuid: undefined, ...PayloadTimestampDefaults(), }) return CreateDecryptedItemFromPayload(payload) } - /** - * We require both a client-side component and a server-side component in generating a - * salt. This way, a comprimised server cannot benefit from sending the same seed value - * for every user. We mix a client-controlled value that is globally unique - * (their identifier), with a server controlled value to produce a salt for our KDF. - * @param identifier - * @param seed - */ - private async generateSalt004(identifier: string, seed: string) { - const hash = await this.crypto.sha256([identifier, seed].join(PARTITION_CHARACTER)) - return Utils.truncateHexString(hash, V004Algorithm.ArgonSaltLength) + createRandomizedKeySystemRootKey(dto: { systemIdentifier: KeySystemIdentifier }): KeySystemRootKeyInterface { + const usecase = new CreateRandomKeySystemRootKey(this.crypto) + return usecase.execute(dto) } - /** - * Computes a root key given a passworf - * qwd and previous keyParams - * @param password - Plain string representing raw user password - * @param keyParams - KeyParams object - */ - public async computeRootKey(password: string, keyParams: SNRootKeyParams): Promise { - return this.deriveKey(password, keyParams) + createUserInputtedKeySystemRootKey(dto: { + systemIdentifier: KeySystemIdentifier + userInputtedPassword: string + }): KeySystemRootKeyInterface { + const usecase = new CreateUserInputKeySystemRootKey(this.crypto) + return usecase.execute(dto) } - /** - * Creates a new root key given an identifier and a user password - * @param identifier - Plain string representing a unique identifier - * @param password - Plain string representing raw user password - */ - public async createRootKey( + deriveUserInputtedKeySystemRootKey(dto: { + keyParams: KeySystemRootKeyParamsInterface + userInputtedPassword: string + }): KeySystemRootKeyInterface { + const usecase = new DeriveKeySystemRootKeyUseCase(this.crypto) + return usecase.execute({ + keyParams: dto.keyParams, + password: dto.userInputtedPassword, + }) + } + + public createKeySystemItemsKey( + uuid: string, + keySystemIdentifier: KeySystemIdentifier, + sharedVaultUuid: string | undefined, + rootKeyToken: string, + ): KeySystemItemsKeyInterface { + const usecase = new CreateKeySystemItemsKeyUseCase(this.crypto) + return usecase.execute({ uuid, keySystemIdentifier, sharedVaultUuid, rootKeyToken }) + } + + public async computeRootKey( + password: Utf8String, + keyParams: SNRootKeyParams, + ): Promise { + const usecase = new DeriveRootKeyUseCase(this.crypto) + return usecase.execute(password, keyParams) + } + + public async createRootKey( identifier: string, - password: string, + password: Utf8String, origination: KeyParamsOrigination, - ): Promise { - const version = ProtocolVersion.V004 - const seed = this.crypto.generateRandomKey(V004Algorithm.ArgonSaltSeedLength) - const keyParams = Create004KeyParams({ - identifier: identifier, - pw_nonce: seed, - version: version, - origination: origination, - created: `${Date.now()}`, - }) - return this.deriveKey(password, keyParams) + ): Promise { + const usecase = new CreateRootKeyUseCase(this.crypto) + return usecase.execute(identifier, password, origination) } - /** - * @param plaintext - The plaintext to encrypt. - * @param rawKey - The key to use to encrypt the plaintext. - * @param nonce - The nonce for encryption. - * @param authenticatedData - JavaScript object (will be stringified) representing - 'Additional authenticated data': data you want to be included in authentication. - */ - encryptString004(plaintext: string, rawKey: string, nonce: string, authenticatedData: ItemAuthenticatedData) { - if (!nonce) { - throw 'encryptString null nonce' - } - if (!rawKey) { - throw 'encryptString null rawKey' - } - return this.crypto.xchacha20Encrypt(plaintext, nonce, rawKey, this.authenticatedDataToString(authenticatedData)) - } - - /** - * @param ciphertext The encrypted text to decrypt. - * @param rawKey The key to use to decrypt the ciphertext. - * @param nonce The nonce for decryption. - * @param rawAuthenticatedData String representing - 'Additional authenticated data' - data you want to be included in authentication. - */ - private decryptString004(ciphertext: string, rawKey: string, nonce: string, rawAuthenticatedData: string) { - return this.crypto.xchacha20Decrypt(ciphertext, nonce, rawKey, rawAuthenticatedData) - } - - generateEncryptionNonce(): string { - return this.crypto.generateRandomKey(V004Algorithm.EncryptionNonceLength) - } - - /** - * @param plaintext The plaintext text to decrypt. - * @param rawKey The key to use to encrypt the plaintext. - */ - generateEncryptedProtocolString(plaintext: string, rawKey: string, authenticatedData: ItemAuthenticatedData) { - const nonce = this.generateEncryptionNonce() - - const ciphertext = this.encryptString004(plaintext, rawKey, nonce, authenticatedData) - - const components: V004StringComponents = [ - ProtocolVersion.V004 as string, - nonce, - ciphertext, - this.authenticatedDataToString(authenticatedData), - ] - - return components.join(PARTITION_CHARACTER) - } - - deconstructEncryptedPayloadString(payloadString: string): V004Components { - const components = payloadString.split(PARTITION_CHARACTER) as V004StringComponents - - return { - version: components[0], - nonce: components[1], - ciphertext: components[2], - authenticatedData: components[3], - } - } - - public getPayloadAuthenticatedData( - encrypted: EncryptedParameters, + public getPayloadAuthenticatedDataForExternalUse( + encrypted: EncryptedOutputParameters, ): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined { - const itemKeyComponents = this.deconstructEncryptedPayloadString(encrypted.enc_item_key) - const authenticatedDataString = itemKeyComponents.authenticatedData - const result = this.stringToAuthenticatedData(authenticatedDataString) - - return result + const usecase = new GetPayloadAuthenticatedDataDetachedUseCase(this.crypto) + return usecase.execute(encrypted) } - /** - * For items that are encrypted with a root key, we append the root key's key params, so - * that in the event the client/user loses a reference to their root key, they may still - * decrypt data by regenerating the key based on the attached key params. - */ - private generateAuthenticatedDataForPayload( - payload: Models.DecryptedPayloadInterface, - key: ItemsKeyInterface | SNRootKey, - ): ItemAuthenticatedData | RootKeyEncryptedAuthenticatedData { - const baseData: ItemAuthenticatedData = { - u: payload.uuid, - v: ProtocolVersion.V004, - } - if (ContentTypeUsesRootKeyEncryption(payload.content_type)) { - return { - ...baseData, - kp: (key as SNRootKey).keyParams.content, - } - } else { - if (!isItemsKey(key)) { - throw Error('Attempting to use non-items key for regular item.') - } - return baseData - } + public generateEncryptedParameters( + payload: DecryptedPayloadInterface, + key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface, + signingKeyPair?: PkcKeyPair, + ): EncryptedOutputParameters { + const usecase = new GenerateEncryptedParametersUseCase(this.crypto) + return usecase.execute(payload, key, signingKeyPair) } - private authenticatedDataToString(attachedData: ItemAuthenticatedData) { - return this.crypto.base64Encode(JSON.stringify(Utils.sortedCopy(Utils.omitUndefinedCopy(attachedData)))) - } - - private stringToAuthenticatedData( - rawAuthenticatedData: string, - override?: Partial, - ): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData { - const base = JSON.parse(this.crypto.base64Decode(rawAuthenticatedData)) - return Utils.sortedCopy({ - ...base, - ...override, - }) - } - - public generateEncryptedParametersSync( - payload: Models.DecryptedPayloadInterface, - key: ItemsKeyInterface | SNRootKey, - ): EncryptedParameters { - const itemKey = this.crypto.generateRandomKey(V004Algorithm.EncryptionKeyLength) - - const contentPlaintext = JSON.stringify(payload.content) - const authenticatedData = this.generateAuthenticatedDataForPayload(payload, key) - const encryptedContentString = this.generateEncryptedProtocolString(contentPlaintext, itemKey, authenticatedData) - - const encryptedItemKey = this.generateEncryptedProtocolString(itemKey, key.itemsKey, authenticatedData) - - return { - uuid: payload.uuid, - items_key_id: isItemsKey(key) ? key.uuid : undefined, - content: encryptedContentString, - enc_item_key: encryptedItemKey, - version: this.version, - } - } - - public generateDecryptedParametersSync( - encrypted: EncryptedParameters, - key: ItemsKeyInterface | SNRootKey, + public generateDecryptedParameters( + encrypted: EncryptedInputParameters, + key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface, ): DecryptedParameters | ErrorDecryptingParameters { - const contentKeyComponents = this.deconstructEncryptedPayloadString(encrypted.enc_item_key) - const authenticatedData = this.stringToAuthenticatedData(contentKeyComponents.authenticatedData, { - u: encrypted.uuid, - v: encrypted.version, - }) + const usecase = new GenerateDecryptedParametersUseCase(this.crypto) + return usecase.execute(encrypted, key) + } - const useAuthenticatedString = this.authenticatedDataToString(authenticatedData) - const contentKey = this.decryptString004( - contentKeyComponents.ciphertext, - key.itemsKey, - contentKeyComponents.nonce, - useAuthenticatedString, - ) + public asymmetricEncrypt(dto: { + stringToEncrypt: Utf8String + senderKeyPair: PkcKeyPair + senderSigningKeyPair: PkcKeyPair + recipientPublicKey: HexString + }): AsymmetricallyEncryptedString { + const usecase = new AsymmetricEncryptUseCase(this.crypto) + return usecase.execute(dto) + } - if (!contentKey) { - console.error('Error decrypting itemKey parameters', encrypted) - return { - uuid: encrypted.uuid, - errorDecrypting: true, - } - } + asymmetricDecrypt(dto: { + stringToDecrypt: AsymmetricallyEncryptedString + recipientSecretKey: HexString + }): AsymmetricDecryptResult | null { + const usecase = new AsymmetricDecryptUseCase(this.crypto) + return usecase.execute(dto) + } - const contentComponents = this.deconstructEncryptedPayloadString(encrypted.content) - const content = this.decryptString004( - contentComponents.ciphertext, - contentKey, - contentComponents.nonce, - useAuthenticatedString, - ) + asymmetricSignatureVerifyDetached( + encryptedString: AsymmetricallyEncryptedString, + ): AsymmetricSignatureVerificationDetachedResult { + const usecase = new AsymmetricSignatureVerificationDetachedUseCase(this.crypto) + return usecase.execute({ encryptedString }) + } - if (!content) { - return { - uuid: encrypted.uuid, - errorDecrypting: true, - } - } else { - return { - uuid: encrypted.uuid, - content: JSON.parse(content), - } + getSenderPublicKeySetFromAsymmetricallyEncryptedString(string: AsymmetricallyEncryptedString): PublicKeySet { + const [_, __, ___, additionalDataString] = string.split(':') + const parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(this.crypto) + const additionalData = parseBase64Usecase.execute(additionalDataString) + return { + encryption: additionalData.senderPublicKey, + signing: additionalData.signingData.publicKey, } } - private async deriveKey(password: string, keyParams: SNRootKeyParams): Promise { - const salt = await this.generateSalt004(keyParams.content004.identifier, keyParams.content004.pw_nonce) - const derivedKey = this.crypto.argon2( - password, - salt, - V004Algorithm.ArgonIterations, - V004Algorithm.ArgonMemLimit, - V004Algorithm.ArgonOutputKeyBytes, - ) - - const partitions = Utils.splitString(derivedKey, 2) - const masterKey = partitions[0] - const serverPassword = partitions[1] - - return CreateNewRootKey({ - masterKey, - serverPassword, - version: ProtocolVersion.V004, - keyParams: keyParams.getPortableValue(), - }) + versionForAsymmetricallyEncryptedString(string: string): ProtocolVersion { + const [versionPrefix] = string.split(':') + const version = versionPrefix.split('_')[0] + return version as ProtocolVersion } } diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricDecrypt.spec.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricDecrypt.spec.ts new file mode 100644 index 000000000..58ae00d0f --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricDecrypt.spec.ts @@ -0,0 +1,81 @@ +import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { getMockedCrypto } from '../../MockedCrypto' +import { AsymmetricDecryptUseCase } from './AsymmetricDecrypt' +import { AsymmetricEncryptUseCase } from './AsymmetricEncrypt' +import { V004AsymmetricStringComponents } from '../../V004AlgorithmTypes' +import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData' + +describe('asymmetric decrypt use case', () => { + let crypto: PureCryptoInterface + let usecase: AsymmetricDecryptUseCase + let recipientKeyPair: PkcKeyPair + let senderKeyPair: PkcKeyPair + let senderSigningKeyPair: PkcKeyPair + + beforeEach(() => { + crypto = getMockedCrypto() + usecase = new AsymmetricDecryptUseCase(crypto) + recipientKeyPair = crypto.sodiumCryptoBoxSeedKeypair('recipient-seedling') + senderKeyPair = crypto.sodiumCryptoBoxSeedKeypair('sender-seedling') + senderSigningKeyPair = crypto.sodiumCryptoSignSeedKeypair('sender-signing-seedling') + }) + + const getEncryptedString = () => { + const encryptUsecase = new AsymmetricEncryptUseCase(crypto) + + const result = encryptUsecase.execute({ + stringToEncrypt: 'foobar', + senderKeyPair: senderKeyPair, + senderSigningKeyPair: senderSigningKeyPair, + recipientPublicKey: recipientKeyPair.publicKey, + }) + + return result + } + + it('should generate decrypted string', () => { + const encryptedString = getEncryptedString() + + const decrypted = usecase.execute({ + stringToDecrypt: encryptedString, + recipientSecretKey: recipientKeyPair.privateKey, + }) + + expect(decrypted).toEqual({ + plaintext: 'foobar', + signatureVerified: true, + signaturePublicKey: senderSigningKeyPair.publicKey, + senderPublicKey: senderKeyPair.publicKey, + }) + }) + + it('should fail signature verification if signature is changed', () => { + const encryptedString = getEncryptedString() + + const [version, nonce, ciphertext] = encryptedString.split(':') + + const corruptAdditionalData: AsymmetricItemAdditionalData = { + signingData: { + publicKey: senderSigningKeyPair.publicKey, + signature: 'corrupt', + }, + senderPublicKey: senderKeyPair.publicKey, + } + + const corruptedAdditionalDataString = crypto.base64Encode(JSON.stringify(corruptAdditionalData)) + + const corruptEncryptedString = [version, nonce, ciphertext, corruptedAdditionalDataString].join(':') + + const decrypted = usecase.execute({ + stringToDecrypt: corruptEncryptedString, + recipientSecretKey: recipientKeyPair.privateKey, + }) + + expect(decrypted).toEqual({ + plaintext: 'foobar', + signatureVerified: false, + signaturePublicKey: senderSigningKeyPair.publicKey, + senderPublicKey: senderKeyPair.publicKey, + }) + }) +}) diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricDecrypt.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricDecrypt.ts new file mode 100644 index 000000000..2cd3c899b --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricDecrypt.ts @@ -0,0 +1,48 @@ +import { HexString, PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { AsymmetricallyEncryptedString } from '../../../Types/Types' +import { V004AsymmetricStringComponents } from '../../V004AlgorithmTypes' +import { ParseConsistentBase64JsonPayloadUseCase } from '../Utils/ParseConsistentBase64JsonPayload' +import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData' +import { AsymmetricDecryptResult } from '../../../Types/AsymmetricDecryptResult' + +export class AsymmetricDecryptUseCase { + private parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(this.crypto) + + constructor(private readonly crypto: PureCryptoInterface) {} + + execute(dto: { + stringToDecrypt: AsymmetricallyEncryptedString + recipientSecretKey: HexString + }): AsymmetricDecryptResult | null { + const [_, nonce, ciphertext, additionalDataString] = dto.stringToDecrypt.split(':') + + const additionalData = this.parseBase64Usecase.execute(additionalDataString) + + try { + const plaintext = this.crypto.sodiumCryptoBoxEasyDecrypt( + ciphertext, + nonce, + additionalData.senderPublicKey, + dto.recipientSecretKey, + ) + if (!plaintext) { + return null + } + + const signatureVerified = this.crypto.sodiumCryptoSignVerify( + ciphertext, + additionalData.signingData.signature, + additionalData.signingData.publicKey, + ) + + return { + plaintext, + signatureVerified, + signaturePublicKey: additionalData.signingData.publicKey, + senderPublicKey: additionalData.senderPublicKey, + } + } catch (error) { + return null + } + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricEncrypt.spec.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricEncrypt.spec.ts new file mode 100644 index 000000000..aee4a58ca --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricEncrypt.spec.ts @@ -0,0 +1,45 @@ +import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { getMockedCrypto } from '../../MockedCrypto' +import { AsymmetricEncryptUseCase } from './AsymmetricEncrypt' +import { V004AsymmetricStringComponents } from '../../V004AlgorithmTypes' +import { ParseConsistentBase64JsonPayloadUseCase } from '../Utils/ParseConsistentBase64JsonPayload' +import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData' + +describe('asymmetric encrypt use case', () => { + let crypto: PureCryptoInterface + let usecase: AsymmetricEncryptUseCase + let encryptionKeyPair: PkcKeyPair + let signingKeyPair: PkcKeyPair + let parseBase64Usecase: ParseConsistentBase64JsonPayloadUseCase + + beforeEach(() => { + crypto = getMockedCrypto() + usecase = new AsymmetricEncryptUseCase(crypto) + encryptionKeyPair = crypto.sodiumCryptoBoxSeedKeypair('seedling') + signingKeyPair = crypto.sodiumCryptoSignSeedKeypair('seedling') + parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(crypto) + }) + + it('should generate encrypted string', () => { + const recipientKeyPair = crypto.sodiumCryptoBoxSeedKeypair('recipient-seedling') + + const result = usecase.execute({ + stringToEncrypt: 'foobar', + senderKeyPair: encryptionKeyPair, + senderSigningKeyPair: signingKeyPair, + recipientPublicKey: recipientKeyPair.publicKey, + }) + + const [version, nonce, ciphertext, additionalDataString] = result.split(':') + + expect(version).toEqual('004_Asym') + expect(nonce).toEqual(expect.any(String)) + expect(ciphertext).toEqual(expect.any(String)) + expect(additionalDataString).toEqual(expect.any(String)) + + const additionalData = parseBase64Usecase.execute(additionalDataString) + expect(additionalData.signingData.publicKey).toEqual(signingKeyPair.publicKey) + expect(additionalData.signingData.signature).toEqual(expect.any(String)) + expect(additionalData.senderPublicKey).toEqual(encryptionKeyPair.publicKey) + }) +}) diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricEncrypt.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricEncrypt.ts new file mode 100644 index 000000000..5a51523b4 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricEncrypt.ts @@ -0,0 +1,45 @@ +import { HexString, PkcKeyPair, PureCryptoInterface, Utf8String } from '@standardnotes/sncrypto-common' +import { AsymmetricallyEncryptedString } from '../../../Types/Types' +import { V004Algorithm } from '../../../../Algorithm' +import { V004AsymmetricCiphertextPrefix, V004AsymmetricStringComponents } from '../../V004AlgorithmTypes' +import { CreateConsistentBase64JsonPayloadUseCase } from '../Utils/CreateConsistentBase64JsonPayload' +import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData' + +export class AsymmetricEncryptUseCase { + private base64DataUsecase = new CreateConsistentBase64JsonPayloadUseCase(this.crypto) + + constructor(private readonly crypto: PureCryptoInterface) {} + + execute(dto: { + stringToEncrypt: Utf8String + senderKeyPair: PkcKeyPair + senderSigningKeyPair: PkcKeyPair + recipientPublicKey: HexString + }): AsymmetricallyEncryptedString { + const nonce = this.crypto.generateRandomKey(V004Algorithm.AsymmetricEncryptionNonceLength) + + const ciphertext = this.crypto.sodiumCryptoBoxEasyEncrypt( + dto.stringToEncrypt, + nonce, + dto.senderKeyPair.privateKey, + dto.recipientPublicKey, + ) + + const additionalData: AsymmetricItemAdditionalData = { + signingData: { + publicKey: dto.senderSigningKeyPair.publicKey, + signature: this.crypto.sodiumCryptoSign(ciphertext, dto.senderSigningKeyPair.privateKey), + }, + senderPublicKey: dto.senderKeyPair.publicKey, + } + + const components: V004AsymmetricStringComponents = [ + V004AsymmetricCiphertextPrefix, + nonce, + ciphertext, + this.base64DataUsecase.execute(additionalData), + ] + + return components.join(':') + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricSignatureVerificationDetached.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricSignatureVerificationDetached.ts new file mode 100644 index 000000000..36e272ad5 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Asymmetric/AsymmetricSignatureVerificationDetached.ts @@ -0,0 +1,36 @@ +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { AsymmetricallyEncryptedString } from '../../../Types/Types' +import { V004AsymmetricStringComponents } from '../../V004AlgorithmTypes' +import { ParseConsistentBase64JsonPayloadUseCase } from '../Utils/ParseConsistentBase64JsonPayload' +import { AsymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData' +import { AsymmetricSignatureVerificationDetachedResult } from '../../../Types/AsymmetricSignatureVerificationDetachedResult' + +export class AsymmetricSignatureVerificationDetachedUseCase { + private parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(this.crypto) + + constructor(private readonly crypto: PureCryptoInterface) {} + + execute(dto: { encryptedString: AsymmetricallyEncryptedString }): AsymmetricSignatureVerificationDetachedResult { + const [_, __, ciphertext, additionalDataString] = dto.encryptedString.split(':') + + const additionalData = this.parseBase64Usecase.execute(additionalDataString) + + try { + const signatureVerified = this.crypto.sodiumCryptoSignVerify( + ciphertext, + additionalData.signingData.signature, + additionalData.signingData.publicKey, + ) + + return { + signatureVerified, + signaturePublicKey: additionalData.signingData.publicKey, + senderPublicKey: additionalData.senderPublicKey, + } + } catch (error) { + return { + signatureVerified: false, + } + } + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Hash/DeriveHashingKey.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Hash/DeriveHashingKey.ts new file mode 100644 index 000000000..eb0360b67 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Hash/DeriveHashingKey.ts @@ -0,0 +1,28 @@ +import { + ItemsKeyInterface, + KeySystemItemsKeyInterface, + KeySystemRootKeyInterface, + RootKeyInterface, +} from '@standardnotes/models' +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { V004Algorithm } from '../../../../Algorithm' +import { HashingKey } from './HashingKey' + +export class DeriveHashingKeyUseCase { + constructor(private readonly crypto: PureCryptoInterface) {} + + execute( + key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface, + ): HashingKey { + const hashingKey = this.crypto.sodiumCryptoKdfDeriveFromKey( + key.itemsKey, + V004Algorithm.PayloadKeyHashingKeySubKeyNumber, + V004Algorithm.PayloadKeyHashingKeySubKeyBytes, + V004Algorithm.PayloadKeyHashingKeySubKeyContext, + ) + + return { + key: hashingKey, + } + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Hash/HashString.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Hash/HashString.ts new file mode 100644 index 000000000..7d5f79e3e --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Hash/HashString.ts @@ -0,0 +1,10 @@ +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { HashingKey } from './HashingKey' + +export class HashStringUseCase { + constructor(private readonly crypto: PureCryptoInterface) {} + + execute(string: string, hashingKey: HashingKey): string { + return this.crypto.sodiumCryptoGenericHash(string, hashingKey.key) + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Hash/HashingKey.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Hash/HashingKey.ts new file mode 100644 index 000000000..21f9530d0 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Hash/HashingKey.ts @@ -0,0 +1,3 @@ +export interface HashingKey { + key: string +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateKeySystemItemsKey.ts b/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateKeySystemItemsKey.ts new file mode 100644 index 000000000..3671e4203 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateKeySystemItemsKey.ts @@ -0,0 +1,45 @@ +import { + CreateDecryptedItemFromPayload, + DecryptedPayload, + DecryptedTransferPayload, + FillItemContentSpecialized, + KeySystemIdentifier, + KeySystemItemsKeyContentSpecialized, + KeySystemItemsKeyInterface, + PayloadTimestampDefaults, +} from '@standardnotes/models' +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { V004Algorithm } from '../../../../Algorithm' +import { ContentType, ProtocolVersion } from '@standardnotes/common' + +export class CreateKeySystemItemsKeyUseCase { + constructor(private readonly crypto: PureCryptoInterface) {} + + execute(dto: { + uuid: string + keySystemIdentifier: KeySystemIdentifier + sharedVaultUuid: string | undefined + rootKeyToken: string + }): KeySystemItemsKeyInterface { + const key = this.crypto.generateRandomKey(V004Algorithm.EncryptionKeyLength) + const content = FillItemContentSpecialized({ + itemsKey: key, + creationTimestamp: new Date().getTime(), + version: ProtocolVersion.V004, + rootKeyToken: dto.rootKeyToken, + }) + + const transferPayload: DecryptedTransferPayload = { + uuid: dto.uuid, + content_type: ContentType.KeySystemItemsKey, + key_system_identifier: dto.keySystemIdentifier, + shared_vault_uuid: dto.sharedVaultUuid, + content: content, + dirty: true, + ...PayloadTimestampDefaults(), + } + + const payload = new DecryptedPayload(transferPayload) + return CreateDecryptedItemFromPayload(payload) + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateRandomKeySystemRootKey.ts b/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateRandomKeySystemRootKey.ts new file mode 100644 index 000000000..542d3a72b --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateRandomKeySystemRootKey.ts @@ -0,0 +1,35 @@ +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { V004Algorithm } from '../../../../Algorithm' +import { + KeySystemRootKeyInterface, + KeySystemRootKeyParamsInterface, + KeySystemRootKeyPasswordType, +} from '@standardnotes/models' +import { ProtocolVersion } from '@standardnotes/common' +import { DeriveKeySystemRootKeyUseCase } from './DeriveKeySystemRootKey' + +export class CreateRandomKeySystemRootKey { + constructor(private readonly crypto: PureCryptoInterface) {} + + execute(dto: { systemIdentifier: string }): KeySystemRootKeyInterface { + const version = ProtocolVersion.V004 + + const seed = this.crypto.generateRandomKey(V004Algorithm.ArgonSaltSeedLength) + + const randomPassword = this.crypto.generateRandomKey(32) + + const keyParams: KeySystemRootKeyParamsInterface = { + systemIdentifier: dto.systemIdentifier, + passwordType: KeySystemRootKeyPasswordType.Randomized, + creationTimestamp: new Date().getTime(), + seed, + version, + } + + const usecase = new DeriveKeySystemRootKeyUseCase(this.crypto) + return usecase.execute({ + password: randomPassword, + keyParams, + }) + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateUserInputKeySystemRootKey.ts b/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateUserInputKeySystemRootKey.ts new file mode 100644 index 000000000..2ba88a6a3 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/CreateUserInputKeySystemRootKey.ts @@ -0,0 +1,34 @@ +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { V004Algorithm } from '../../../../Algorithm' +import { + KeySystemIdentifier, + KeySystemRootKeyInterface, + KeySystemRootKeyParamsInterface, + KeySystemRootKeyPasswordType, +} from '@standardnotes/models' +import { ProtocolVersion } from '@standardnotes/common' +import { DeriveKeySystemRootKeyUseCase } from './DeriveKeySystemRootKey' + +export class CreateUserInputKeySystemRootKey { + constructor(private readonly crypto: PureCryptoInterface) {} + + execute(dto: { systemIdentifier: KeySystemIdentifier; userInputtedPassword: string }): KeySystemRootKeyInterface { + const version = ProtocolVersion.V004 + + const seed = this.crypto.generateRandomKey(V004Algorithm.ArgonSaltSeedLength) + + const keyParams: KeySystemRootKeyParamsInterface = { + systemIdentifier: dto.systemIdentifier, + passwordType: KeySystemRootKeyPasswordType.UserInputted, + creationTimestamp: new Date().getTime(), + seed, + version, + } + + const usecase = new DeriveKeySystemRootKeyUseCase(this.crypto) + return usecase.execute({ + password: dto.userInputtedPassword, + keyParams, + }) + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/DeriveKeySystemRootKey.ts b/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/DeriveKeySystemRootKey.ts new file mode 100644 index 000000000..db6790320 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/KeySystem/DeriveKeySystemRootKey.ts @@ -0,0 +1,60 @@ +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { UuidGenerator, splitString, truncateHexString } from '@standardnotes/utils' +import { V004PartitionCharacter } from '../../V004AlgorithmTypes' +import { V004Algorithm } from '../../../../Algorithm' +import { + DecryptedPayload, + FillItemContentSpecialized, + KeySystemRootKey, + KeySystemRootKeyContent, + KeySystemRootKeyContentSpecialized, + KeySystemRootKeyInterface, + PayloadTimestampDefaults, + KeySystemRootKeyParamsInterface, +} from '@standardnotes/models' +import { ContentType, ProtocolVersion } from '@standardnotes/common' + +export class DeriveKeySystemRootKeyUseCase { + constructor(private readonly crypto: PureCryptoInterface) {} + + execute(dto: { password: string; keyParams: KeySystemRootKeyParamsInterface }): KeySystemRootKeyInterface { + const seed = dto.keyParams.seed + const salt = this.generateSalt(dto.keyParams.systemIdentifier, seed) + const derivedKey = this.crypto.argon2( + dto.password, + salt, + V004Algorithm.ArgonIterations, + V004Algorithm.ArgonMemLimit, + V004Algorithm.ArgonOutputKeyBytes, + ) + + const partitions = splitString(derivedKey, 2) + const masterKey = partitions[0] + const token = partitions[1] + + const uuid = UuidGenerator.GenerateUuid() + + const content: KeySystemRootKeyContentSpecialized = { + systemIdentifier: dto.keyParams.systemIdentifier, + key: masterKey, + keyVersion: ProtocolVersion.V004, + keyParams: dto.keyParams, + token, + } + + const payload = new DecryptedPayload({ + uuid: uuid, + content_type: ContentType.KeySystemRootKey, + content: FillItemContentSpecialized(content), + ...PayloadTimestampDefaults(), + }) + + return new KeySystemRootKey(payload) + } + + private generateSalt(identifier: string, seed: string) { + const hash = this.crypto.sodiumCryptoGenericHash([identifier, seed].join(V004PartitionCharacter)) + + return truncateHexString(hash, V004Algorithm.ArgonSaltLength) + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/RootKey/CreateRootKey.ts b/packages/encryption/src/Domain/Operator/004/UseCase/RootKey/CreateRootKey.ts new file mode 100644 index 000000000..e51d8a3a3 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/RootKey/CreateRootKey.ts @@ -0,0 +1,29 @@ +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { V004Algorithm } from '../../../../Algorithm' +import { RootKeyInterface } from '@standardnotes/models' +import { KeyParamsOrigination, ProtocolVersion } from '@standardnotes/common' +import { DeriveRootKeyUseCase } from './DeriveRootKey' +import { Create004KeyParams } from '../../../../Keys/RootKey/KeyParamsFunctions' + +export class CreateRootKeyUseCase { + constructor(private readonly crypto: PureCryptoInterface) {} + + async execute( + identifier: string, + password: string, + origination: KeyParamsOrigination, + ): Promise { + const version = ProtocolVersion.V004 + const seed = this.crypto.generateRandomKey(V004Algorithm.ArgonSaltSeedLength) + const keyParams = Create004KeyParams({ + identifier: identifier, + pw_nonce: seed, + version: version, + origination: origination, + created: `${Date.now()}`, + }) + + const usecase = new DeriveRootKeyUseCase(this.crypto) + return usecase.execute(password, keyParams) + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/RootKey/DeriveRootKey.ts b/packages/encryption/src/Domain/Operator/004/UseCase/RootKey/DeriveRootKey.ts new file mode 100644 index 000000000..57186d0af --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/RootKey/DeriveRootKey.ts @@ -0,0 +1,66 @@ +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { splitString, truncateHexString } from '@standardnotes/utils' +import { V004PartitionCharacter } from '../../V004AlgorithmTypes' +import { V004Algorithm } from '../../../../Algorithm' +import { RootKeyInterface } from '@standardnotes/models' +import { SNRootKeyParams } from '../../../../Keys/RootKey/RootKeyParams' +import { CreateNewRootKey } from '../../../../Keys/RootKey/Functions' +import { ProtocolVersion } from '@standardnotes/common' + +export class DeriveRootKeyUseCase { + constructor(private readonly crypto: PureCryptoInterface) {} + + async execute(password: string, keyParams: SNRootKeyParams): Promise { + const seed = keyParams.content004.pw_nonce + const salt = await this.generateSalt(keyParams.content004.identifier, seed) + const derivedKey = this.crypto.argon2( + password, + salt, + V004Algorithm.ArgonIterations, + V004Algorithm.ArgonMemLimit, + V004Algorithm.ArgonOutputKeyBytes, + ) + + const partitions = splitString(derivedKey, 2) + const masterKey = partitions[0] + const serverPassword = partitions[1] + + const encryptionKeyPairSeed = this.crypto.sodiumCryptoKdfDeriveFromKey( + masterKey, + V004Algorithm.MasterKeyEncryptionKeyPairSubKeyNumber, + V004Algorithm.MasterKeyEncryptionKeyPairSubKeyBytes, + V004Algorithm.MasterKeyEncryptionKeyPairSubKeyContext, + ) + const encryptionKeyPair = this.crypto.sodiumCryptoBoxSeedKeypair(encryptionKeyPairSeed) + + const signingKeyPairSeed = this.crypto.sodiumCryptoKdfDeriveFromKey( + masterKey, + V004Algorithm.MasterKeySigningKeyPairSubKeyNumber, + V004Algorithm.MasterKeySigningKeyPairSubKeyBytes, + V004Algorithm.MasterKeySigningKeyPairSubKeyContext, + ) + const signingKeyPair = this.crypto.sodiumCryptoSignSeedKeypair(signingKeyPairSeed) + + return CreateNewRootKey({ + masterKey, + serverPassword, + version: ProtocolVersion.V004, + keyParams: keyParams.getPortableValue(), + encryptionKeyPair, + signingKeyPair, + }) + } + + /** + * We require both a client-side component and a server-side component in generating a + * salt. This way, a comprimised server cannot benefit from sending the same seed value + * for every user. We mix a client-controlled value that is globally unique + * (their identifier), with a server controlled value to produce a salt for our KDF. + * @param identifier + * @param seed + */ + private async generateSalt(identifier: string, seed: string) { + const hash = await this.crypto.sha256([identifier, seed].join(V004PartitionCharacter)) + return truncateHexString(hash, V004Algorithm.ArgonSaltLength) + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateAuthenticatedData.spec.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateAuthenticatedData.spec.ts new file mode 100644 index 000000000..0b409b4cb --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateAuthenticatedData.spec.ts @@ -0,0 +1,111 @@ +import { CreateAnyKeyParams } from '../../../../Keys/RootKey/KeyParamsFunctions' +import { AnyKeyParamsContent, ContentType, ProtocolVersion } from '@standardnotes/common' +import { GenerateAuthenticatedDataUseCase } from './GenerateAuthenticatedData' +import { + DecryptedPayloadInterface, + ItemsKeyInterface, + KeySystemRootKeyInterface, + RootKeyInterface, +} from '@standardnotes/models' +import { KeySystemItemsKey } from '../../../../Keys/KeySystemItemsKey/KeySystemItemsKey' + +describe('generate authenticated data use case', () => { + let usecase: GenerateAuthenticatedDataUseCase + + beforeEach(() => { + usecase = new GenerateAuthenticatedDataUseCase() + }) + + it('should include key params if payload being encrypted is an items key', () => { + const payload = { + uuid: '123', + content_type: ContentType.ItemsKey, + } as jest.Mocked + + const keyParams = CreateAnyKeyParams({ + identifier: 'key-params-123', + } as jest.Mocked) + + const rootKey = { + keyParams, + } as jest.Mocked + + const authenticatedData = usecase.execute(payload, rootKey) + + expect(authenticatedData).toEqual({ + u: payload.uuid, + v: ProtocolVersion.V004, + kp: keyParams.content, + }) + }) + + it('should include root key params if payload is a key system items key', () => { + const payload = { + uuid: '123', + content_type: ContentType.KeySystemItemsKey, + shared_vault_uuid: 'shared-vault-uuid-123', + key_system_identifier: 'key-system-identifier-123', + } as jest.Mocked + + const keySystemRootKey = { + keyVersion: ProtocolVersion.V004, + keyParams: { + seed: 'seed-123', + }, + content_type: ContentType.KeySystemRootKey, + token: '123', + } as jest.Mocked + + const authenticatedData = usecase.execute(payload, keySystemRootKey) + + expect(authenticatedData).toEqual({ + u: payload.uuid, + v: ProtocolVersion.V004, + kp: keySystemRootKey.keyParams, + ksi: payload.key_system_identifier, + svu: payload.shared_vault_uuid, + }) + }) + + it('should include key system identifier and shared vault uuid', () => { + const payload = { + uuid: '123', + content_type: ContentType.Note, + shared_vault_uuid: 'shared-vault-uuid-123', + key_system_identifier: 'key-system-identifier-123', + } as jest.Mocked + + const itemsKey = { + creationTimestamp: 123, + keyVersion: ProtocolVersion.V004, + content_type: ContentType.KeySystemItemsKey, + } as jest.Mocked + + const authenticatedData = usecase.execute(payload, itemsKey) + + expect(authenticatedData).toEqual({ + u: payload.uuid, + v: ProtocolVersion.V004, + ksi: payload.key_system_identifier, + svu: payload.shared_vault_uuid, + }) + }) + + it('should include only uuid and version if non-keysystem item with items key', () => { + const payload = { + uuid: '123', + content_type: ContentType.Note, + } as jest.Mocked + + const itemsKey = { + content_type: ContentType.ItemsKey, + } as jest.Mocked + + const authenticatedData = usecase.execute(payload, itemsKey) + + expect(authenticatedData).toEqual({ + u: payload.uuid, + v: ProtocolVersion.V004, + }) + }) +}) diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateAuthenticatedData.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateAuthenticatedData.ts new file mode 100644 index 000000000..6f31e7290 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateAuthenticatedData.ts @@ -0,0 +1,58 @@ +import { + DecryptedPayloadInterface, + ItemsKeyInterface, + KeySystemItemsKeyInterface, + KeySystemRootKeyInterface, + RootKeyInterface, + isKeySystemRootKey, + ContentTypeUsesRootKeyEncryption, + ContentTypeUsesKeySystemRootKeyEncryption, +} from '@standardnotes/models' +import { ItemAuthenticatedData } from '../../../../Types/ItemAuthenticatedData' +import { RootKeyEncryptedAuthenticatedData } from '../../../../Types/RootKeyEncryptedAuthenticatedData' +import { KeySystemItemsKeyAuthenticatedData } from '../../../../Types/KeySystemItemsKeyAuthenticatedData' +import { ProtocolVersion } from '@standardnotes/common' +import { isItemsKey } from '../../../../Keys/ItemsKey/ItemsKey' +import { isKeySystemItemsKey } from '../../../../Keys/KeySystemItemsKey/KeySystemItemsKey' + +export class GenerateAuthenticatedDataUseCase { + execute( + payload: DecryptedPayloadInterface, + key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface, + ): ItemAuthenticatedData | RootKeyEncryptedAuthenticatedData | KeySystemItemsKeyAuthenticatedData { + const baseData: ItemAuthenticatedData = { + u: payload.uuid, + v: ProtocolVersion.V004, + } + + if (payload.key_system_identifier) { + baseData.ksi = payload.key_system_identifier + } + + if (payload.shared_vault_uuid) { + baseData.svu = payload.shared_vault_uuid + } + + if (ContentTypeUsesRootKeyEncryption(payload.content_type)) { + return { + ...baseData, + kp: (key as RootKeyInterface).keyParams.content, + } + } else if (ContentTypeUsesKeySystemRootKeyEncryption(payload.content_type)) { + if (!isKeySystemRootKey(key)) { + throw Error( + `Attempting to use non-key system root key ${key.content_type} for item content type ${payload.content_type}`, + ) + } + return { + ...baseData, + kp: key.keyParams, + } + } else { + if (!isItemsKey(key) && !isKeySystemItemsKey(key)) { + throw Error('Attempting to use non-items key for regular item.') + } + return baseData + } + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateDecryptedParameters.spec.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateDecryptedParameters.spec.ts new file mode 100644 index 000000000..db5583165 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateDecryptedParameters.spec.ts @@ -0,0 +1,80 @@ +import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { getMockedCrypto } from '../../MockedCrypto' +import { GenerateDecryptedParametersUseCase } from './GenerateDecryptedParameters' +import { ContentType } from '@standardnotes/common' +import { DecryptedPayloadInterface, ItemsKeyInterface } from '@standardnotes/models' +import { GenerateEncryptedParametersUseCase } from './GenerateEncryptedParameters' +import { EncryptedInputParameters, EncryptedOutputParameters } from '../../../../Types/EncryptedParameters' + +describe('generate decrypted parameters usecase', () => { + let crypto: PureCryptoInterface + let usecase: GenerateDecryptedParametersUseCase + let signingKeyPair: PkcKeyPair + let itemsKey: ItemsKeyInterface + + beforeEach(() => { + crypto = getMockedCrypto() + usecase = new GenerateDecryptedParametersUseCase(crypto) + itemsKey = { + uuid: 'items-key-id', + itemsKey: 'items-key', + content_type: ContentType.ItemsKey, + } as jest.Mocked + }) + + const generateEncryptedParameters = (plaintext: string) => { + const decrypted = { + uuid: '123', + content: { + text: plaintext, + }, + content_type: ContentType.Note, + } as unknown as jest.Mocked + + const encryptedParametersUsecase = new GenerateEncryptedParametersUseCase(crypto) + return encryptedParametersUsecase.execute(decrypted, itemsKey, signingKeyPair) as T + } + + describe('without signatures', () => { + it('should generate decrypted parameters', () => { + const encrypted = generateEncryptedParameters('foo') + + const result = usecase.execute(encrypted, itemsKey) + + expect(result).toEqual({ + uuid: expect.any(String), + content: expect.any(Object), + signatureData: { + required: false, + contentHash: expect.any(String), + }, + }) + }) + }) + + describe('with signatures', () => { + beforeEach(() => { + signingKeyPair = crypto.sodiumCryptoSignSeedKeypair('seedling') + }) + + it('should generate decrypted parameters', () => { + const encrypted = generateEncryptedParameters('foo') + + const result = usecase.execute(encrypted, itemsKey) + + expect(result).toEqual({ + uuid: expect.any(String), + content: expect.any(Object), + signatureData: { + required: false, + contentHash: expect.any(String), + result: { + passes: true, + publicKey: signingKeyPair.publicKey, + signature: expect.any(String), + }, + }, + }) + }) + }) +}) diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateDecryptedParameters.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateDecryptedParameters.ts new file mode 100644 index 000000000..8168136c5 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateDecryptedParameters.ts @@ -0,0 +1,140 @@ +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { deconstructEncryptedPayloadString } from '../../V004AlgorithmHelpers' +import { + ItemContent, + ItemsKeyInterface, + KeySystemItemsKeyInterface, + KeySystemRootKeyInterface, + RootKeyInterface, +} from '@standardnotes/models' +import { StringToAuthenticatedDataUseCase } from '../Utils/StringToAuthenticatedData' +import { CreateConsistentBase64JsonPayloadUseCase } from '../Utils/CreateConsistentBase64JsonPayload' +import { GenerateSymmetricPayloadSignatureResultUseCase } from './GenerateSymmetricPayloadSignatureResult' +import { + EncryptedInputParameters, + EncryptedOutputParameters, + ErrorDecryptingParameters, +} from './../../../../Types/EncryptedParameters' +import { DecryptedParameters } from '../../../../Types/DecryptedParameters' +import { DeriveHashingKeyUseCase } from '../Hash/DeriveHashingKey' + +export class GenerateDecryptedParametersUseCase { + private base64DataUsecase = new CreateConsistentBase64JsonPayloadUseCase(this.crypto) + private stringToAuthenticatedDataUseCase = new StringToAuthenticatedDataUseCase(this.crypto) + private signingVerificationUseCase = new GenerateSymmetricPayloadSignatureResultUseCase(this.crypto) + private deriveHashingKeyUseCase = new DeriveHashingKeyUseCase(this.crypto) + + constructor(private readonly crypto: PureCryptoInterface) {} + + execute( + encrypted: EncryptedInputParameters, + key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface, + ): DecryptedParameters | ErrorDecryptingParameters { + const contentKeyResult = this.decryptContentKey(encrypted, key) + if (!contentKeyResult) { + console.error('Error decrypting contentKey from parameters', encrypted) + return { + uuid: encrypted.uuid, + errorDecrypting: true, + } + } + + const contentResult = this.decryptContent(encrypted, contentKeyResult.contentKey) + if (!contentResult) { + return { + uuid: encrypted.uuid, + errorDecrypting: true, + } + } + + const hashingKey = this.deriveHashingKeyUseCase.execute(key) + + const signatureVerificationResult = this.signingVerificationUseCase.execute( + encrypted, + hashingKey, + { + additionalData: contentKeyResult.components.additionalData, + plaintext: contentKeyResult.contentKey, + }, + { + additionalData: contentResult.components.additionalData, + plaintext: contentResult.content, + }, + ) + + return { + uuid: encrypted.uuid, + content: JSON.parse(contentResult.content), + signatureData: signatureVerificationResult, + } + } + + private decryptContent(encrypted: EncryptedOutputParameters, contentKey: string) { + const contentComponents = deconstructEncryptedPayloadString(encrypted.content) + + const contentAuthenticatedData = this.stringToAuthenticatedDataUseCase.execute( + contentComponents.authenticatedData, + { + u: encrypted.uuid, + v: encrypted.version, + ksi: encrypted.key_system_identifier, + svu: encrypted.shared_vault_uuid, + }, + ) + + const authenticatedDataString = this.base64DataUsecase.execute(contentAuthenticatedData) + + const content = this.crypto.xchacha20Decrypt( + contentComponents.ciphertext, + contentComponents.nonce, + contentKey, + authenticatedDataString, + ) + + if (!content) { + return null + } + + return { + content, + components: contentComponents, + authenticatedDataString, + } + } + + private decryptContentKey( + encrypted: EncryptedOutputParameters, + key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface, + ) { + const contentKeyComponents = deconstructEncryptedPayloadString(encrypted.enc_item_key) + + const contentKeyAuthenticatedData = this.stringToAuthenticatedDataUseCase.execute( + contentKeyComponents.authenticatedData, + { + u: encrypted.uuid, + v: encrypted.version, + ksi: encrypted.key_system_identifier, + svu: encrypted.shared_vault_uuid, + }, + ) + + const authenticatedDataString = this.base64DataUsecase.execute(contentKeyAuthenticatedData) + + const contentKey = this.crypto.xchacha20Decrypt( + contentKeyComponents.ciphertext, + contentKeyComponents.nonce, + key.itemsKey, + authenticatedDataString, + ) + + if (!contentKey) { + return null + } + + return { + contentKey, + components: contentKeyComponents, + authenticatedDataString, + } + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateEncryptedParameters.spec.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateEncryptedParameters.spec.ts new file mode 100644 index 000000000..ed80094b8 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateEncryptedParameters.spec.ts @@ -0,0 +1,137 @@ +import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { getMockedCrypto } from '../../MockedCrypto' +import { AnyKeyParamsContent, ContentType, ProtocolVersion } from '@standardnotes/common' +import { GenerateEncryptedParametersUseCase } from './GenerateEncryptedParameters' +import { + DecryptedPayloadInterface, + ItemsKeyInterface, + KeySystemRootKeyInterface, + RootKeyInterface, +} from '@standardnotes/models' +import { deconstructEncryptedPayloadString } from '../../V004AlgorithmHelpers' +import { ParseConsistentBase64JsonPayloadUseCase } from '../Utils/ParseConsistentBase64JsonPayload' +import { SymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData' + +describe('generate encrypted parameters usecase', () => { + let crypto: PureCryptoInterface + let usecase: GenerateEncryptedParametersUseCase + + beforeEach(() => { + crypto = getMockedCrypto() + usecase = new GenerateEncryptedParametersUseCase(crypto) + }) + + describe('without signing keypair', () => { + it('should generate encrypted parameters', () => { + const decrypted = { + uuid: '123', + content: { + title: 'title', + text: 'text', + }, + content_type: ContentType.Note, + } as unknown as jest.Mocked + + const itemsKey = { + uuid: 'items-key-id', + itemsKey: 'items-key', + content_type: ContentType.ItemsKey, + } as jest.Mocked + + const result = usecase.execute(decrypted, itemsKey) + + expect(result).toEqual({ + uuid: '123', + content_type: ContentType.Note, + items_key_id: 'items-key-id', + content: expect.any(String), + enc_item_key: expect.any(String), + version: ProtocolVersion.V004, + rawSigningDataClientOnly: undefined, + }) + }) + + it('should not include items_key_id if item to encrypt is items key payload', () => { + const decrypted = { + uuid: '123', + content: { + foo: 'bar', + }, + content_type: ContentType.ItemsKey, + } as unknown as jest.Mocked + + const rootKey = { + uuid: 'items-key-id', + itemsKey: 'items-key', + keyParams: { + content: {} as jest.Mocked, + }, + content_type: ContentType.RootKey, + } as jest.Mocked + + const result = usecase.execute(decrypted, rootKey) + + expect(result.items_key_id).toBeUndefined() + }) + + it('should not include items_key_id if item to encrypt is key system items key payload', () => { + const decrypted = { + uuid: '123', + content: { + foo: 'bar', + }, + content_type: ContentType.KeySystemItemsKey, + } as unknown as jest.Mocked + + const rootKey = { + uuid: 'items-key-id', + itemsKey: 'items-key', + content_type: ContentType.KeySystemRootKey, + } as jest.Mocked + + const result = usecase.execute(decrypted, rootKey) + + expect(result.items_key_id).toBeUndefined() + }) + }) + + describe('with signing keypair', () => { + let signingKeyPair: PkcKeyPair + let parseBase64Usecase: ParseConsistentBase64JsonPayloadUseCase + + beforeEach(() => { + signingKeyPair = crypto.sodiumCryptoSignSeedKeypair('seedling') + parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(crypto) + }) + + it('encrypted string should include additional data', () => { + const decrypted = { + uuid: '123', + content: { + title: 'title', + text: 'text', + }, + content_type: ContentType.Note, + } as unknown as jest.Mocked + + const itemsKey = { + uuid: 'items-key-id', + itemsKey: 'items-key', + content_type: ContentType.ItemsKey, + } as jest.Mocked + + const result = usecase.execute(decrypted, itemsKey, signingKeyPair) + + const contentComponents = deconstructEncryptedPayloadString(result.content) + + const additionalData = parseBase64Usecase.execute(contentComponents.additionalData) + + expect(additionalData).toEqual({ + signingData: { + signature: expect.any(String), + publicKey: signingKeyPair.publicKey, + }, + }) + }) + }) +}) diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateEncryptedParameters.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateEncryptedParameters.ts new file mode 100644 index 000000000..cbd2cce47 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateEncryptedParameters.ts @@ -0,0 +1,120 @@ +import { ProtocolVersion } from '@standardnotes/common' +import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { + DecryptedPayloadInterface, + ItemsKeyInterface, + KeySystemItemsKeyInterface, + KeySystemRootKeyInterface, + RootKeyInterface, +} from '@standardnotes/models' +import { CreateConsistentBase64JsonPayloadUseCase } from '../Utils/CreateConsistentBase64JsonPayload' +import { doesPayloadRequireSigning } from '../../V004AlgorithmHelpers' +import { EncryptedOutputParameters } from '../../../../Types/EncryptedParameters' +import { GenerateAuthenticatedDataUseCase } from './GenerateAuthenticatedData' +import { GenerateEncryptedProtocolStringUseCase } from './GenerateEncryptedProtocolString' +import { GenerateSymmetricAdditionalDataUseCase } from './GenerateSymmetricAdditionalData' +import { isItemsKey } from '../../../../Keys/ItemsKey/ItemsKey' +import { isKeySystemItemsKey } from '../../../../Keys/KeySystemItemsKey/KeySystemItemsKey' +import { ItemAuthenticatedData } from '../../../../Types/ItemAuthenticatedData' +import { V004Algorithm } from '../../../../Algorithm' +import { AdditionalData } from '../../../../Types/EncryptionAdditionalData' +import { HashingKey } from '../Hash/HashingKey' +import { DeriveHashingKeyUseCase } from '../Hash/DeriveHashingKey' + +export class GenerateEncryptedParametersUseCase { + private generateProtocolStringUseCase = new GenerateEncryptedProtocolStringUseCase(this.crypto) + private generateAuthenticatedDataUseCase = new GenerateAuthenticatedDataUseCase() + private generateAdditionalDataUseCase = new GenerateSymmetricAdditionalDataUseCase(this.crypto) + private encodeBase64DataUsecase = new CreateConsistentBase64JsonPayloadUseCase(this.crypto) + private deriveHashingKeyUseCase = new DeriveHashingKeyUseCase(this.crypto) + + constructor(private readonly crypto: PureCryptoInterface) {} + + execute( + payload: DecryptedPayloadInterface, + key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface, + signingKeyPair?: PkcKeyPair, + ): EncryptedOutputParameters { + if (doesPayloadRequireSigning(payload) && !signingKeyPair) { + throw Error('Payload requires signing but no signing key pair was provided.') + } + + const commonAuthenticatedData = this.generateAuthenticatedDataUseCase.execute(payload, key) + + const hashingKey = this.deriveHashingKeyUseCase.execute(key) + + const { contentKey, encryptedContentKey } = this.generateEncryptedContentKey( + key, + hashingKey, + commonAuthenticatedData, + signingKeyPair, + ) + + const { encryptedContent } = this.generateEncryptedContent( + payload, + hashingKey, + contentKey, + commonAuthenticatedData, + signingKeyPair, + ) + + return { + uuid: payload.uuid, + content_type: payload.content_type, + items_key_id: isItemsKey(key) || isKeySystemItemsKey(key) ? key.uuid : undefined, + content: encryptedContent, + enc_item_key: encryptedContentKey, + version: ProtocolVersion.V004, + key_system_identifier: payload.key_system_identifier, + shared_vault_uuid: payload.shared_vault_uuid, + } + } + + private generateEncryptedContent( + payload: DecryptedPayloadInterface, + hashingKey: HashingKey, + contentKey: string, + commonAuthenticatedData: ItemAuthenticatedData, + signingKeyPair?: PkcKeyPair, + ): { + encryptedContent: string + } { + const content = JSON.stringify(payload.content) + + const { additionalData } = this.generateAdditionalDataUseCase.execute(content, hashingKey, signingKeyPair) + + const encryptedContent = this.generateProtocolStringUseCase.execute( + content, + contentKey, + this.encodeBase64DataUsecase.execute(commonAuthenticatedData), + this.encodeBase64DataUsecase.execute(additionalData), + ) + + return { + encryptedContent, + } + } + + private generateEncryptedContentKey( + key: ItemsKeyInterface | KeySystemItemsKeyInterface | KeySystemRootKeyInterface | RootKeyInterface, + hashingKey: HashingKey, + commonAuthenticatedData: ItemAuthenticatedData, + signingKeyPair?: PkcKeyPair, + ) { + const contentKey = this.crypto.generateRandomKey(V004Algorithm.EncryptionKeyLength) + + const { additionalData } = this.generateAdditionalDataUseCase.execute(contentKey, hashingKey, signingKeyPair) + + const encryptedContentKey = this.generateProtocolStringUseCase.execute( + contentKey, + key.itemsKey, + this.encodeBase64DataUsecase.execute(commonAuthenticatedData), + this.encodeBase64DataUsecase.execute(additionalData), + ) + + return { + contentKey, + encryptedContentKey, + } + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateEncryptedProtocolString.spec.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateEncryptedProtocolString.spec.ts new file mode 100644 index 000000000..331cdaa04 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateEncryptedProtocolString.spec.ts @@ -0,0 +1,44 @@ +import { ProtocolVersion } from '@standardnotes/common' +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' + +import { ItemAuthenticatedData } from './../../../../Types/ItemAuthenticatedData' +import { GenerateEncryptedProtocolStringUseCase } from './GenerateEncryptedProtocolString' +import { AdditionalData } from '../../../../Types/EncryptionAdditionalData' +import { getMockedCrypto } from '../../MockedCrypto' + +describe('generate encrypted protocol string', () => { + let crypto: PureCryptoInterface + let usecase: GenerateEncryptedProtocolStringUseCase + + beforeEach(() => { + crypto = getMockedCrypto() + usecase = new GenerateEncryptedProtocolStringUseCase(crypto) + }) + + it('should generate encrypted protocol string', () => { + const aad: ItemAuthenticatedData = { + u: '123', + v: ProtocolVersion.V004, + } + + const signingData: AdditionalData = {} + + const nonce = 'noncy' + crypto.generateRandomKey = jest.fn().mockReturnValue(nonce) + + const plaintext = 'foo' + + const result = usecase.execute( + plaintext, + 'secret', + crypto.base64Encode(JSON.stringify(aad)), + crypto.base64Encode(JSON.stringify(signingData)), + ) + + expect(result).toEqual( + `004:${nonce}:${plaintext}:${crypto.base64Encode(JSON.stringify(aad))}:${crypto.base64Encode( + JSON.stringify(signingData), + )}`, + ) + }) +}) diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateEncryptedProtocolString.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateEncryptedProtocolString.ts new file mode 100644 index 000000000..caca147fc --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateEncryptedProtocolString.ts @@ -0,0 +1,41 @@ +import { Base64String, HexString, PureCryptoInterface, Utf8String } from '@standardnotes/sncrypto-common' +import { V004PartitionCharacter, V004StringComponents } from '../../V004AlgorithmTypes' +import { ProtocolVersion } from '@standardnotes/common' +import { V004Algorithm } from '../../../../Algorithm' + +export class GenerateEncryptedProtocolStringUseCase { + constructor(private readonly crypto: PureCryptoInterface) {} + + execute(plaintext: string, rawKey: string, authenticatedData: string, additionalData: string): string { + const nonce = this.crypto.generateRandomKey(V004Algorithm.EncryptionNonceLength) + + const ciphertext = this.encryptString(plaintext, rawKey, nonce, authenticatedData) + + const components: V004StringComponents = [ + ProtocolVersion.V004 as string, + nonce, + ciphertext, + authenticatedData, + additionalData, + ] + + return components.join(V004PartitionCharacter) + } + + encryptString( + plaintext: Utf8String, + rawKey: HexString, + nonce: HexString, + authenticatedData: Utf8String, + ): Base64String { + if (!nonce) { + throw 'encryptString null nonce' + } + + if (!rawKey) { + throw 'encryptString null rawKey' + } + + return this.crypto.xchacha20Encrypt(plaintext, nonce, rawKey, authenticatedData) + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateSymmetricAdditionalData.spec.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateSymmetricAdditionalData.spec.ts new file mode 100644 index 000000000..5b85bfb83 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateSymmetricAdditionalData.spec.ts @@ -0,0 +1,43 @@ +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' + +import { getMockedCrypto } from '../../MockedCrypto' +import { GenerateSymmetricAdditionalDataUseCase } from './GenerateSymmetricAdditionalData' +import { HashingKey } from '../Hash/HashingKey' + +describe('generate symmetric additional data usecase', () => { + let crypto: PureCryptoInterface + let usecase: GenerateSymmetricAdditionalDataUseCase + + beforeEach(() => { + crypto = getMockedCrypto() + usecase = new GenerateSymmetricAdditionalDataUseCase(crypto) + }) + + it('should generate signing data with signing keypair', () => { + const payloadPlaintext = 'foo' + const hashingKey: HashingKey = { key: 'secret-123' } + const signingKeyPair = crypto.sodiumCryptoSignSeedKeypair('seedling') + + const { additionalData, plaintextHash } = usecase.execute(payloadPlaintext, hashingKey, signingKeyPair) + + expect(additionalData).toEqual({ + signingData: { + publicKey: signingKeyPair.publicKey, + signature: crypto.sodiumCryptoSign(plaintextHash, signingKeyPair.privateKey), + }, + }) + + expect(plaintextHash).toEqual(crypto.sodiumCryptoGenericHash(payloadPlaintext, hashingKey.key)) + }) + + it('should generate empty signing data without signing keypair', () => { + const payloadPlaintext = 'foo' + const hashingKey: HashingKey = { key: 'secret-123' } + + const { additionalData, plaintextHash } = usecase.execute(payloadPlaintext, hashingKey, undefined) + + expect(additionalData).toEqual({}) + + expect(plaintextHash).toEqual(crypto.sodiumCryptoGenericHash(payloadPlaintext, hashingKey.key)) + }) +}) diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateSymmetricAdditionalData.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateSymmetricAdditionalData.ts new file mode 100644 index 000000000..8d0f44b6b --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateSymmetricAdditionalData.ts @@ -0,0 +1,37 @@ +import { PkcKeyPair, PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { AdditionalData } from '../../../../Types/EncryptionAdditionalData' +import { HashStringUseCase } from '../Hash/HashString' +import { HashingKey } from '../Hash/HashingKey' + +export class GenerateSymmetricAdditionalDataUseCase { + private hashUseCase = new HashStringUseCase(this.crypto) + + constructor(private readonly crypto: PureCryptoInterface) {} + + execute( + payloadPlaintext: string, + hashingKey: HashingKey, + signingKeyPair?: PkcKeyPair, + ): { additionalData: AdditionalData; plaintextHash: string } { + const plaintextHash = this.hashUseCase.execute(payloadPlaintext, hashingKey) + + if (!signingKeyPair) { + return { + additionalData: {}, + plaintextHash, + } + } + + const signature = this.crypto.sodiumCryptoSign(plaintextHash, signingKeyPair.privateKey) + + return { + additionalData: { + signingData: { + publicKey: signingKeyPair.publicKey, + signature, + }, + }, + plaintextHash, + } + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateSymmetricPayloadSignatureResult.spec.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateSymmetricPayloadSignatureResult.spec.ts new file mode 100644 index 000000000..1ab51e745 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateSymmetricPayloadSignatureResult.spec.ts @@ -0,0 +1,303 @@ +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { getMockedCrypto } from '../../MockedCrypto' +import { EncryptedInputParameters, EncryptedOutputParameters } from '../../../../Types/EncryptedParameters' +import { GenerateSymmetricPayloadSignatureResultUseCase } from './GenerateSymmetricPayloadSignatureResult' +import { GenerateSymmetricAdditionalDataUseCase } from './GenerateSymmetricAdditionalData' +import { CreateConsistentBase64JsonPayloadUseCase } from '../Utils/CreateConsistentBase64JsonPayload' +import { doesPayloadRequireSigning } from '../../V004AlgorithmHelpers' +import { PersistentSignatureData } from '@standardnotes/models' +import { HashStringUseCase } from '../Hash/HashString' +import { HashingKey } from '../Hash/HashingKey' + +describe('generate symmetric signing data usecase', () => { + let crypto: PureCryptoInterface + let usecase: GenerateSymmetricPayloadSignatureResultUseCase + let hashUsecase: HashStringUseCase + let additionalDataUseCase: GenerateSymmetricAdditionalDataUseCase + let encodeUseCase: CreateConsistentBase64JsonPayloadUseCase + + beforeEach(() => { + crypto = getMockedCrypto() + usecase = new GenerateSymmetricPayloadSignatureResultUseCase(crypto) + hashUsecase = new HashStringUseCase(crypto) + additionalDataUseCase = new GenerateSymmetricAdditionalDataUseCase(crypto) + encodeUseCase = new CreateConsistentBase64JsonPayloadUseCase(crypto) + }) + + it('payload with shared vault uuid should require signature', () => { + const payload: Partial = { + shared_vault_uuid: '456', + } + + expect(doesPayloadRequireSigning(payload)).toBe(true) + }) + + it('payload with key system identifier only should not require signature', () => { + const payload: Partial = { + key_system_identifier: '123', + } + + expect(doesPayloadRequireSigning(payload)).toBe(false) + }) + + it('payload without key system identifier or shared vault uuid should not require signature', () => { + const payload: Partial = { + key_system_identifier: undefined, + shared_vault_uuid: undefined, + } + + expect(doesPayloadRequireSigning(payload)).toBe(false) + }) + + it('signature should be verified with correct parameters', () => { + const payload = { + key_system_identifier: '123', + shared_vault_uuid: '456', + } as jest.Mocked + + const hashingKey: HashingKey = { key: 'secret-123' } + const content = 'contentplaintext' + const contentKey = 'contentkeysecret' + + const keypair = crypto.sodiumCryptoSignSeedKeypair('seedling') + + const contentAdditionalDataResultResult = additionalDataUseCase.execute(content, hashingKey, keypair) + + const contentKeyAdditionalDataResultResult = additionalDataUseCase.execute(contentKey, hashingKey, keypair) + + const result = usecase.execute( + payload, + hashingKey, + { + additionalData: encodeUseCase.execute(contentKeyAdditionalDataResultResult.additionalData), + plaintext: contentKey, + }, + { + additionalData: encodeUseCase.execute(contentAdditionalDataResultResult.additionalData), + plaintext: content, + }, + ) + + expect(result).toEqual({ + required: true, + contentHash: expect.any(String), + result: { + passes: true, + publicKey: keypair.publicKey, + signature: expect.any(String), + }, + }) + }) + + it('should return required false with no result if no signing data is provided and signing is not required', () => { + const payloadWithOptionalSigning = { + key_system_identifier: undefined, + shared_vault_uuid: undefined, + } as jest.Mocked + + const hashingKey: HashingKey = { key: 'secret-123' } + const content = 'contentplaintext' + const contentKey = 'contentkeysecret' + + const contentAdditionalDataResult = additionalDataUseCase.execute(content, hashingKey, undefined) + const contentKeyAdditionalDataResult = additionalDataUseCase.execute(contentKey, hashingKey, undefined) + + const result = usecase.execute( + payloadWithOptionalSigning, + hashingKey, + { + additionalData: encodeUseCase.execute(contentKeyAdditionalDataResult.additionalData), + plaintext: contentKey, + }, + { + additionalData: encodeUseCase.execute(contentAdditionalDataResult.additionalData), + plaintext: content, + }, + ) + + expect(result).toEqual({ + required: false, + contentHash: expect.any(String), + }) + }) + + it('should return required true with fail result if no signing data is provided and signing is required', () => { + const payloadWithRequiredSigning = { + key_system_identifier: '123', + shared_vault_uuid: '456', + } as jest.Mocked + + const hashingKey: HashingKey = { key: 'secret-123' } + const content = 'contentplaintext' + const contentKey = 'contentkeysecret' + + const contentAdditionalDataResult = additionalDataUseCase.execute(content, hashingKey, undefined) + const contentKeyAdditionalDataResult = additionalDataUseCase.execute(contentKey, hashingKey, undefined) + + const result = usecase.execute( + payloadWithRequiredSigning, + hashingKey, + { + additionalData: encodeUseCase.execute(contentKeyAdditionalDataResult.additionalData), + plaintext: contentKey, + }, + { + additionalData: encodeUseCase.execute(contentAdditionalDataResult.additionalData), + plaintext: content, + }, + ) + + expect(result).toEqual({ + required: true, + contentHash: expect.any(String), + result: { + passes: false, + publicKey: '', + signature: '', + }, + }) + }) + + it('should fail if content public key differs from contentKey public key', () => { + const payload = { + key_system_identifier: '123', + shared_vault_uuid: '456', + } as jest.Mocked + + const hashingKey: HashingKey = { key: 'secret-123' } + const content = 'contentplaintext' + const contentKey = 'contentkeysecret' + + const contentKeyPair = crypto.sodiumCryptoSignSeedKeypair('contentseed') + const contentKeyKeyPair = crypto.sodiumCryptoSignSeedKeypair('contentkeyseed') + + const contentAdditionalDataResult = additionalDataUseCase.execute(content, hashingKey, contentKeyPair) + const contentKeyAdditionalDataResult = additionalDataUseCase.execute(contentKey, hashingKey, contentKeyKeyPair) + + const result = usecase.execute( + payload, + hashingKey, + { + additionalData: encodeUseCase.execute(contentKeyAdditionalDataResult.additionalData), + plaintext: contentKey, + }, + { + additionalData: encodeUseCase.execute(contentAdditionalDataResult.additionalData), + plaintext: content, + }, + ) + + expect(result).toEqual({ + required: true, + contentHash: expect.any(String), + result: { + passes: false, + publicKey: '', + signature: '', + }, + }) + }) + + it('if content hash has not changed and previous failing signature is supplied, new result should also be failing', () => { + const hashingKey: HashingKey = { key: 'secret-123' } + const content = 'contentplaintext' + const contentKey = 'contentkeysecret' + const contentHash = hashUsecase.execute(content, hashingKey) + + const previousResult: PersistentSignatureData = { + required: true, + contentHash: contentHash, + result: { + passes: false, + publicKey: '', + signature: '', + }, + } + + const payload = { + key_system_identifier: '123', + shared_vault_uuid: '456', + signatureData: previousResult, + } as jest.Mocked + + const keypair = crypto.sodiumCryptoSignSeedKeypair('seedling') + + const contentAdditionalDataResultResult = additionalDataUseCase.execute(content, hashingKey, keypair) + + const contentKeyAdditionalDataResultResult = additionalDataUseCase.execute(contentKey, hashingKey, keypair) + + const result = usecase.execute( + payload, + hashingKey, + { + additionalData: encodeUseCase.execute(contentKeyAdditionalDataResultResult.additionalData), + plaintext: contentKey, + }, + { + additionalData: encodeUseCase.execute(contentAdditionalDataResultResult.additionalData), + plaintext: content, + }, + ) + + expect(result).toEqual({ + required: true, + contentHash: contentHash, + result: { + passes: false, + publicKey: keypair.publicKey, + signature: expect.any(String), + }, + }) + }) + + it('previous failing signature should be ignored if content hash has changed', () => { + const hashingKey: HashingKey = { key: 'secret-123' } + const content = 'contentplaintext' + const contentKey = 'contentkeysecret' + + const previousResult: PersistentSignatureData = { + required: true, + contentHash: 'different hash', + result: { + passes: false, + publicKey: '', + signature: '', + }, + } + + const payload = { + key_system_identifier: '123', + shared_vault_uuid: '456', + signatureData: previousResult, + } as jest.Mocked + + const keypair = crypto.sodiumCryptoSignSeedKeypair('seedling') + + const contentAdditionalDataResultResult = additionalDataUseCase.execute(content, hashingKey, keypair) + + const contentKeyAdditionalDataResultResult = additionalDataUseCase.execute(contentKey, hashingKey, keypair) + + const result = usecase.execute( + payload, + hashingKey, + { + additionalData: encodeUseCase.execute(contentKeyAdditionalDataResultResult.additionalData), + plaintext: contentKey, + }, + { + additionalData: encodeUseCase.execute(contentAdditionalDataResultResult.additionalData), + plaintext: content, + }, + ) + + expect(result).toEqual({ + required: true, + contentHash: expect.any(String), + result: { + passes: true, + publicKey: keypair.publicKey, + signature: expect.any(String), + }, + }) + }) +}) diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateSymmetricPayloadSignatureResult.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateSymmetricPayloadSignatureResult.ts new file mode 100644 index 000000000..35f71748b --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GenerateSymmetricPayloadSignatureResult.ts @@ -0,0 +1,127 @@ +import { EncryptedInputParameters } from '../../../../Types/EncryptedParameters' +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { doesPayloadRequireSigning } from '../../V004AlgorithmHelpers' +import { ParseConsistentBase64JsonPayloadUseCase } from '../Utils/ParseConsistentBase64JsonPayload' +import { SymmetricItemAdditionalData } from '../../../../Types/EncryptionAdditionalData' +import { HashStringUseCase } from '../Hash/HashString' +import { PersistentSignatureData } from '@standardnotes/models' +import { HashingKey } from '../Hash/HashingKey' + +/** + * Embedded signatures check the signature on the symmetric string, but this string can change every time we encrypt + * the payload, even though its content hasn't changed. This would mean that if we received a signed payload from User B, + * then saved this payload into local storage by encrypting it, we would lose the signature of the content it came with, and + * it would instead be overwritten by our local user signature, which would always pass. + * + * In addition to embedded signature verification, we'll also hang on to a sticky signature of the content, which + * remains the same until the hash changes. We do not perform any static verification on this data; instead, clients + * can compute authenticity of the content on demand. + */ +export class GenerateSymmetricPayloadSignatureResultUseCase { + private parseBase64Usecase = new ParseConsistentBase64JsonPayloadUseCase(this.crypto) + private hashUseCase = new HashStringUseCase(this.crypto) + + constructor(private readonly crypto: PureCryptoInterface) {} + + execute( + payload: EncryptedInputParameters, + hashingKey: HashingKey, + contentKeyParameters: { + additionalData: string + plaintext: string + }, + contentParameters: { + additionalData: string + plaintext: string + }, + ): PersistentSignatureData { + const contentKeyHash = this.hashUseCase.execute(contentKeyParameters.plaintext, hashingKey) + + const contentHash = this.hashUseCase.execute(contentParameters.plaintext, hashingKey) + + const contentKeyAdditionalData = this.parseBase64Usecase.execute( + contentKeyParameters.additionalData, + ) + + const contentAdditionalData = this.parseBase64Usecase.execute( + contentParameters.additionalData, + ) + + const verificationRequired = doesPayloadRequireSigning(payload) + + if (!contentKeyAdditionalData.signingData || !contentAdditionalData.signingData) { + if (verificationRequired) { + return { + required: true, + contentHash: contentHash, + result: { + passes: false, + publicKey: '', + signature: '', + }, + } + } + return { + required: false, + contentHash: contentHash, + } + } + + if (contentKeyAdditionalData.signingData.publicKey !== contentAdditionalData.signingData.publicKey) { + return { + required: verificationRequired, + contentHash: contentHash, + result: { + passes: false, + publicKey: '', + signature: '', + }, + } + } + + const commonPublicKey = contentKeyAdditionalData.signingData.publicKey + + const contentKeySignatureVerified = this.verifySignature( + contentKeyHash, + contentKeyAdditionalData.signingData.signature, + commonPublicKey, + ) + + const contentSignatureVerified = this.verifySignature( + contentHash, + contentAdditionalData.signingData.signature, + commonPublicKey, + ) + + let passesStickyContentVerification = true + const previousSignatureResult = payload.signatureData + if (previousSignatureResult) { + const previousSignatureStillApplicable = previousSignatureResult.contentHash === contentHash + + if (previousSignatureStillApplicable) { + if (previousSignatureResult.required) { + passesStickyContentVerification = previousSignatureResult.result.passes + } else if (previousSignatureResult.result) { + passesStickyContentVerification = previousSignatureResult.result.passes + } + } + } + + const passesAllVerification = + contentKeySignatureVerified && contentSignatureVerified && passesStickyContentVerification + + return { + required: verificationRequired, + contentHash: contentHash, + result: { + passes: passesAllVerification, + publicKey: commonPublicKey, + signature: contentAdditionalData.signingData.signature, + }, + } + } + + private verifySignature(contentHash: string, signature: string, publicKey: string) { + return this.crypto.sodiumCryptoSignVerify(contentHash, signature, publicKey) + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GetPayloadAuthenticatedDataDetached.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GetPayloadAuthenticatedDataDetached.ts new file mode 100644 index 000000000..9c0b802f2 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Symmetric/GetPayloadAuthenticatedDataDetached.ts @@ -0,0 +1,27 @@ +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { EncryptedOutputParameters } from '../../../../Types/EncryptedParameters' +import { RootKeyEncryptedAuthenticatedData } from '../../../../Types/RootKeyEncryptedAuthenticatedData' +import { ItemAuthenticatedData } from '../../../../Types/ItemAuthenticatedData' +import { LegacyAttachedData } from '../../../../Types/LegacyAttachedData' +import { deconstructEncryptedPayloadString } from '../../V004AlgorithmHelpers' +import { ParseConsistentBase64JsonPayloadUseCase } from '../Utils/ParseConsistentBase64JsonPayload' + +export class GetPayloadAuthenticatedDataDetachedUseCase { + private parseStringUseCase = new ParseConsistentBase64JsonPayloadUseCase(this.crypto) + + constructor(private readonly crypto: PureCryptoInterface) {} + + execute( + encrypted: EncryptedOutputParameters, + ): RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData | undefined { + const itemKeyComponents = deconstructEncryptedPayloadString(encrypted.enc_item_key) + + const authenticatedDataString = itemKeyComponents.authenticatedData + + const result = this.parseStringUseCase.execute< + RootKeyEncryptedAuthenticatedData | ItemAuthenticatedData | LegacyAttachedData + >(authenticatedDataString) + + return result + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Utils/CreateConsistentBase64JsonPayload.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Utils/CreateConsistentBase64JsonPayload.ts new file mode 100644 index 000000000..306659d6e --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Utils/CreateConsistentBase64JsonPayload.ts @@ -0,0 +1,10 @@ +import { Base64String, PureCryptoInterface } from '@standardnotes/sncrypto-common' +import * as Utils from '@standardnotes/utils' + +export class CreateConsistentBase64JsonPayloadUseCase { + constructor(private readonly crypto: PureCryptoInterface) {} + + execute(jsonObject: T): Base64String { + return this.crypto.base64Encode(JSON.stringify(Utils.sortedCopy(Utils.omitUndefinedCopy(jsonObject)))) + } +} diff --git a/packages/encryption/src/Domain/Operator/004/UseCase/Utils/ParseConsistentBase64JsonPayload.ts b/packages/encryption/src/Domain/Operator/004/UseCase/Utils/ParseConsistentBase64JsonPayload.ts new file mode 100644 index 000000000..90a32bba9 --- /dev/null +++ b/packages/encryption/src/Domain/Operator/004/UseCase/Utils/ParseConsistentBase64JsonPayload.ts @@ -0,0 +1,9 @@ +import { PureCryptoInterface } from '@standardnotes/sncrypto-common' + +export class ParseConsistentBase64JsonPayloadUseCase { + constructor(private readonly crypto: PureCryptoInterface) {} + + execute

3)_H!vpE{Qa=}^p(RFUxtdA)4ag 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≶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
eh`V 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
%3B`01@2X zDez1qi6U1xI_UiD=rFK3W+9!r%@+t zfknd-2**j9TMpK1AL1O5FRpi-#wHjiVenRcWkXpyU(je6yX|e zI)8zp%M7~wy4u!8T;xZdy)Ri0qO(;FUAoXMSbgmk_rigb!$7!MR|dQw2<*C4>>~Uo ziJGsyF;^DzYtjF0|dNL7F4|Fqa7#W=)_TvU@1HztZPg%NdMQ53y zy`i0_SwJ(l9W^h^L>^DmR#)aK<}Wl8D=Tb@c%Uh|PVs&w(&OkHDZ5|7xd!49FL=!} zg3*a~=c$ydchsntmIV zUi(nNY{uQ&s1@POxaHkAp&W$xDl{7Q2 za-llqv}g|Z(3NV5*_4CRt65ltO12*z`ETqtP?#)9M{C;h&^{J~VW}9=eUe1tUSLwW zuEMgzgPCXBW9<3tu~EPI;kv!O=8N?R>5s;|Hn4ax-i>~3eIvZ1FHJ5BeDx(07ZnuJ z!PXH2MlSn%0U9^)j%haAu%)P4ifN=T?eGR)<9hr4wAkHxp+$fm?OrUE9H{D~9^8;k z5AEyz_meC~Vbpu+_ap%q|8IH!4^*58!)7^R(gXJ!t;I9PEtMx$G{g z$iZI|6$;EUkZ}-`*Bgf558mDw3}F`Tz~7Us)M$D{=$;2`{omTGd@L>}JLWaW7_yBl z`hAy*WA)V*WVShD%U^L49=z(qUmrSCSqKd;!aTGG=m%-~^ps|jDrg9COBP*YtyQ4q zp&fQwydh&5i}i3*pB%O$yp{^7Yl*m?^ANHCt9Y#gy1Ia)ra9!%(4_-}q?*S?^7YSd z^h>)-h(8OrR3_S^F=m1L4yK?pBX{Ru5i=0!u(Jbf3m*u%*JOFW%VCT3Jt}2)7462E zF8xcthlDALt-Ac)K3cLf0M}4OL=w*Zb+Vb|j0mUddbYZj)kh+ z$ys8WSXT)qHB`Lx5MB!*mb)Yp1ju&!6%X4Pko@fgz8*t}=N9B)4uu!kLThyRgwZHA z>0A-qogs~q3d-?0=CIg94|SgV;z!cveJQS;B7`|SjJjrR$WHC^g`BD|*do z)tW@d@=`9jGdJh_V%h=$`muZUCG_EKy5c}3gJGYvk#smV*ai8&i2G~gq*I`| z%LMj0C_$Gh2dGAsfUT|?Af*MaL)obSa0M%iFI)wGVFQxFXbHV2;^h7Fs2-cSy?%*z=P*-waYCezL8si zEaM(2!OQ;arC*+c^rl7~|06xTcAfUe=s;`}4sQ1oh8q2EasfVr&^V@hWL`XS<8QD% z?K^0m85ml>q^dHbkrJ8O$4>#-??Sd_zT@lhfweeFzqf{XJ=EL~far)40y-6zuBum| zLwLoB6-ytDf}jab|3I}jGiXjvjF1Po%ubtQ3q z{RpJTJ=_kuv<}^~YwwOzTS%u2SpvOZ(oPH%`wzvlT-*)&YdX<(R(203ku*n%@hT01 zemWW3KYR4~I<7j3$I>x&JFNMckBkhy*;2QrtBx%Q(Hwp5al7LD?siBbqblB)pq^PP zz7Z$4OPW!P6rx_MwuzZsdzAy;IQLVl5{gk+FlTFVd&?eQAGF?klOUDI65&UW5`2lI zht?yZSWEvHhO$%>wYSYyM19z5S~~n(&R}f?ezLtXUDysYNU5^p#o+>LWG~5a_jz}u z^i}*e%PZ}uik+#@>^Xv&^X+{u#Plh3nW8a08b151-cZv>h^&dlm0U( zSd82MKDRi2!uo%pptPB{{LrnezBX#rs%1})(6&LWa{_#^T~E{P+g9?SWobI??b0LJ zR9_~H6lJ7pDkCDQJ1$9a_E3gePuK> zW{nkqBZ?a9SXO~w)%0A=-L(yG7O2bMB^{;j68HMvHpT_5qpFs` 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≺YDarcOKLE~)Jv7L8J4Dmha2uWXoDq^NJ% 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%0D=nxNyQB3$jY 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 MYK3$gpKtC^8J`PvJvgNf9B=QXf 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®SwZp& zrW3!vMg%H}U`r`-I{CVR4 zN2&h*uj0dhKl)Me<2L>D@FLHz;R+gI!bPxFVfFl0SPCdBjpeZ!(0a&L%Wz7-AD%EW z^1pHW$GP^IT&piNqjIAR(r$lJmz(}h#A2I9?pGipCanU$5Ct)|!+be~bE34-%-9Z~ zE;ZJ!02NU8^G0^n$I)lytA~1(QVxMbfuTYonlaBpfoMzaNByl6SFq$9j%ye>uC}C6 zgUwnQ2VMq^T~Eq($*R6tXAJU}%P%$xS93!`GziV$I`f2v2Lpa3lKC!vBei}&MmhMkQI+i6;Bx#fDv@FXg&?# zuv_+j+VW1@C|wdX1NB+!SZj0A8trvUqAlB=ch)quT|xeW>vE`Obg2Cjaf_{uxejGIw(Rk(B-!Qfb1p{0aZM>+l^MS`HgkDbDgrJUhtzdumi& zr@d}|J+yC6ju;PrpNE)h9hLv>(*Ypvk0=p$nMKwlq32q64Yu;BMGw!o@4ta}$@7M> zu?>cCPvSY85KE@W{!47C)h~t4|H}jGo!09cc53hi3#zchb%%yMfg6HY4v7qhpW6@Y z<9RLU5O)VI$M60IQ&v_MUrvy97XnH<7sU;o0(_6ih_Dw$$bIZ!P|+c+$T4|z5Nu=o zMXNYX=W9A7WP@$#^9%HRHLoYGtCiue9KXUhx zsG9-$xn1m|u^Fm3S7f4Km`+E=lW5eyOLK_fV8jga;#&71FGDkyLvbzC&$c zHpi^SVKx*zw{62zzR}I(j~9!BN?-;G;l~?+Ta*}c0JpDK0najBV4eP*Ha3{)&ZMjM zbp_&sj9~5H;>sk9GdT(X?RR9d9?hEhi_zf}M(N0`X3Os)ZGmhi(z1$N3bP{)A?cLf z=g%LlJMoPk!A+x!=}gm!)o-$}qJpQ`uu!r)ae+(KSoneFB*lEsY`|?-s+~n5N%TvE z9wT#Jyh{#C&5ES{wX;JPys)6m=1=i9C1ZUfAyg-3wO1AY*-A_Bd#z15;&I@H6D1YO zS~#oQM3X#FDjiDr17ocTX5ri4{-Nx@>4>Rx%9%e>)0iF%joFm9a$6=Ho$)pYhMBGK z!&@LDCyr03A^G`KTdN8 zL15$G)XiZGKgibDUQN^SW^QhO9N%56b+q~PG{3aB0Ko-JC`h*5;a=kP5H^3!ohzX+(+y1@s*&n--sqO@@>^T^Ia)5Rq+8q^lY#Fv z%y|k|eF=>$X1FWP$>b``DXL1F@iL+@bl7|W=@b;o8t?imo}2hoQP;XaSpmNqA5S5oeK3XsFDsCB{6hrK zhBuEjN8Jq*SUS`v6lsa74)Fp-h&UNIE6!*9GgOWpYPwFkIW4N(OqIm@>DUqV>2PzU zA_zjeLX5{uCv|NzBxc(onP#n(o|)@j;Ia>vPKoAksiWT&t$6vl8WJ~WKn8|Tez`6P zgVa6wpFi>3g#xjrJ5Z_bX@8smdC4z`I!c{kVi=l%eXhNHC{ZP5kdA?%2>A?o>ZX*& zw_9rC?x~o3OqBLGmz&+SuNR{o({9D2b$B@b#);|3&v!%y+ zGy50BUrb`**rnCQf|dTY@WTEC*AnN$I7YZJk2iq^+EMS15ksNp7@s5Ca1UspmSQ?S zNBfgbbqJ^sf|qPe*8O&dx{P1J!zAOO?+zlMm0l4O3Fv; zm@IBjFHg^q@HJy`4-sv5cZ%E;Dr>y6KqLFJg1fnOH7^gD0O69lFO5e}53dj?>tsoo zYXo@+I#eWaE#`>HcVY&ey@swb!FdT{j@?AW1s?O}KwT(hIe#~+Z8&Qiki9$sqH9XQjn#h?Vw588OLU`~QwdgROI>?qs z923a;>&V>=RBqlXfA@AqiImi;JL5VB?BVrcIhKfx2QU5iRHXh4sc3Ml< z^TygbIgiD#FOcVvSueN{!|_WGtG zf9HGrBvJBS1l0iKbfN^X1k3U+3uB(S-R?xOWDPKQQ)Q!^j_bPAy&?$xbLR0FK6Br{ z?Qj12C6iXc3F6V+fHi5|S>;$1$@!x!Uph)cs51(x3xIM^wd1=5=GtBTrS*Dqizct?T#oPK&1I;2^U+Ia}8Z(oz z^ScdIS~`k$EKZ_?!7Eva`OkYMZZOOEckQU3d5JSg`UoJn3@}?J|yQ zqd!uif-tBEv<2(U!-%KtQYKJvnel)Oh!im9ZrnHs3BFYz>p6zE&rV;lYjtJ8c!nqm z4hng@d5bMp1m5FgAD*&MFQN(+kUO0R!S3MKhQi_HfsBks8Br@KCktBsR98$7jr066 zem$Lbh7C1eG4~%sH%)G68UQEC%nmwIS@)uH8(D2t?;?3%-ELu@1xpF=?GU>ciwp@S zO`44Pw_5mLr#Rh$8gHxY2DEyE(=cX+J`l1;i zL;AmZs-Be-NHPh%=-CF@TN$}^L+;f+i>~#XOtIKsRsr&hX0(Lp$8DF!8$B-CwV6X@ zE{lbBx0R2n@#<{^rkAUz5^Ek(C_ z?K>vUfb{+n+ZnyN{qgmwev`L{z4~whtl0pKDcjk&S!{5+G5+z(-0wLkR!T+@>bkWAh(GaIlML z#)Vt7;Mpr5WU~7$^7|#jVK)QSC&HrC|0;0&z0RSgJ+sp=8!^X=qL`4UOA$vs?m^nhfUZxEyNitOt|bW{oB0{EkF** zv~0txmMappiXl*jfn8;D&5~9AJ=Cn4`>Z&7GUre>G#ua@swU>cefY+Flj#yvt0{1! z3A)U4vb#;G%{r{UAoo)U(f&3YWHg<6QiP`?=!rIu{;OFZ9q{tDK;)MT2kzxOz6iM17{BMdJ z_%}szW8-nZU~FuH{6mrBKNQLI4@I{59Z&mrcU5_#b^C%Hd)-BW%*}J%fM%L!1IL$y zCBPEk_Vavs9`>IB+zgcLZ1e;qBp{6>#LBq$3!<5UTn$48ynv^H+YQQRGq``HY8jmd z7(2Y;J~MX5njgX2yK2pO7te$i2?=umXPXJy?x;srGTlUq9Za2JTQ=%GWJsPx8jn^?F4Gmfvq-=rcW?h!&7Ngt^L-cEjAqA%4hZB#Al5=AGs z!-pg~eOr{u3aBs9)<3hcq;75Wv=5360{?b`+J_`R!jkk^YdkTO!}q zFAc+iwQPFX5#!+H?roRnr+IxSE)<&Mfwv;Sh(R+ats&;U7;9WrnnV8E7dAh$M9GVY z>Z883mrN=%5EW=5ZT4BUv=8~!fFyoL?x3G;vIBp@^P0ygp$<#n2BGMw#$DE8*c$CmM88FINiPg#aP0S zjWM4QXDB8%Rth()Bc(JW@-yu|k|X4sxXx(-mjU5c7?-Ma020w)rP)E!XULC4IUxjJ}WRGtwwy zA2nxE666JsCU3ENXK^a9jqcnBp7pUDyQXG47krR~i6k?a4W z$dVt5MEi##zbh|}B+4K^69hFhC5v!ABLX6&b=MFZs@Lq}nT>)Z28mMouou$BzM3U= z0Q_T;rr3*JJ)FItUq3!qHy?dzr>Z;8D_pTEbcyy#g&XpPaZc2~7_MZorL1ssEz19{ z&+?+{KhrCn6jHI_CFx!flxMXtNsNa*BQ*1O#Pi7{f6skwD?8i1Lc5(2K;ys7m6H>0jQH@^eL7%dT1~E!Y4Prv z#_Hf>EHVc@sdcK!T4C9$2(z;aA(aCT^e4oMB_yisS%#c zPAReHy(pNL5%s|@$~sKEVLm{#IXO~yV6Wsti~#_*_?vxu5b3Fg2LxB#UFz|oi<~Yu1Z@?Ka?b*ZTT}-YQ16j=fLH*kLnf^%G~Qibl;iHdjh#eMzPX|5=$(%g{pUs4 z+qN^!F}2wr%6{4W8X%>$Sxm_)UO-D#oY{z)EqcYMR#1a6Epus<<4Ta%r>P+_kE|dNvsXlVPG`wMLu1TI|h)%VLez z!YuRt)?Z}4-0oy?AXk)Cti8y}mTg_u4IwIGI$w;TT(RE5^}14d!g-x5jo1-Ruu*Bj zt!K9GX{L_0)RJV$ht0LtqfvEijO1@o&F6EL|} z17pN<)&PR;2`#!jmaN8dP7HV26nBztRF&!sPz1l%w6+g^NcA4QZ!;7!di$PB^A zt{HI8?>=SGyj%r^^@5gG`kG`MVf=?J^!0D={EmG znm3yNNt)_j&~Z5e9I((skDK{(?k7@`%#)ij7++6M>P2;r=Qq_g% zh2_6xs?54j#lNFvn5DAxW@5|h$|d)1<*Sy6sx`TRN;HXsd@0bnZFXBqgDt9g%S8Dr2A%SV_f#6BAwMHO zbkCMY_XNWPRQ*@Dq)-5bVlUgOChUD@De8!u=ahA>or!gHeA+#|J}d4AB%zvJdamr9 z3;bLjeWcv)Qk4Dbz@~5cps8M9fm&AYrRS8g_tH3Y7Y(ebF419LU=-Ak^fv_v70Nr0 zzQb;Ub{hVwQ?TJ(!4>FRPUtB@M=Q6$ro$}o5~f2Xw3nH_H>^P^Am};rJ^AxpV(?aV zlzsZ0yTq<16FlawvZ+!!S86YBE)QE}FM(BRX zi43f44K4p=pi^D9Sr>)(o>q-i#wYw0F8MJ7i2A27`W_zNKXAkyG18AzjFHGebyItc z75mfE#3cny!AUEZMuVvC?r@n z=q~?+ctNpG1rud|JCniNtT7n}q3J?>kTT>7FKr6cVCPUD140L6E;DaOR zbSDWAR@Mk z7EG}|km~qydZ5Qkp)bKOS7#%+Bet>3ItAvDJz5G%kCnb|QoWf@Emjxy4jjYti8}|X zBz1HT_Ng+S#?^%(0`ecfZxXJ01TW_%A}dLf`g$&8)Bd&n&y*lvT3~57;h!WW_@DDI zZ!@0NF$9Ax#QraCYfX|UW;o4ATcpj?XLgzJ0BgsFkWT~{En);WzlfvOXzfg}e`KBg&E(IfZV#*PbW{pZ2EG`&WG z+^JA}NauBR6I^ulJ%F)TP;C&cNwi*7XJh+b#8E*FyJUW6Zs4PY^FpTCJfvE^S?yP1 zAUs`b<|qR#(-TrqJPT08fh3y6H<*M0*qKY@iQ^g>a}QOcrJd1M>u_}Lmr6+-X5pVx z0G4D}2KEDV9D-b85BTrDiRn|0BbtjDF)K*q-QHkdk17>U^LYDUVvTfvwvSzJb}lIm zXKGyx(nMprDpW0(a@cK#;AJ;!aCQwco)%;^iX@df3yzsSb`onGTOx4x3;dp^_^^5S zQ!btgK`CO$p4N+lh@+*J+tza4x9*wapX@~q&qN=;lu#WyyAjFqE3OApoPya72l&*Q zkQ^4ZhB6(W`$iYy7_$My6;%0Jil|O`pxuO7yFXbzjviO+yM}u#^91FbGc4xnwgf|d z_`yPLncB(t0q2nH+~K^#h(k8K<3J#$X0r{e$*+A=K0>tC>G{ri1p|@oHJsZ#eP-x9 zuYB8)MrS<+>08CMzu#xNLJpx<5^pYui1&nwE}MAMlNq?UY6a`BtP&{yM@#JXCj553^Bs z|7TH$ST+t>=Nk7W4yywhx0quHxfQGjd&z2?_R(o>?B6+wOOsGQyhv;F7Qz7i*sjhI z^M?cThTmp49iDn0jgZS{M!Ld zX;pL?0#h5;B#{6Mv_@+78(Ijyuu*tDlVlF5j9sAoyL&%ki(1{D6*?pCFQL0@=W>Xc zajXR7?;>iYz|#iJ`2I?o|h{Q!}`*YZMxw(NpYB}60*zDZgz%gP@GVL*6b4~SFSdcYhLdW1uGY>$N zO|g8k!&zh&KOw4%tg3|1%y+*U6W=##&67Y!7y*W=a$9b=1s2Obi1Vq7=^5txNR7iM z>>{yE2$8*}P}R%HJ_Ys36WsyE8vdj&LX>q}BnN6Vby7&TZ#*tU-65%khD zYo6~GFbaYZ&C=;4u@4T^9DxFJ3P)2d!nWS5DyE?}9Mvd16Wqr1%q<5?Xqt zjBP>tQEnWe8>}R(98j#J@y&BE5BhyCw4;vwPLD?6mhMz^KXYM37=j#o87eUQ>&AxK}A5)t)zo~4FGg$1F`LhJH;UNl= z1R5KTFcN1aUKze3buwGcEE~lqlOe?;=yjudv&FE|P-F#=5Q`Z-ff$d9&L}Q$uvae9 zDg~0TG3Yr-N}8@b)AUe60>$Z$fh(OWsvbtx#1rP|Y-Q5z44eoU<=Np9cLr>cYZ{2A&a2Dr@ET!r& zMXX8ke#eV&r7%Xf{s}8JWc|>9mHwRHT$D4eO=^coK>P(%@S3y_s{~_(2nna7Xt;@7++qP}nwr$%scG|YBowjY;-kDi> z?^pFuC!${JM6C4#*3%qoj^10t=WuNcg_5aA5sim-JDWq&!P)q5zI!l54NP|sLX}14 z`6jVRL*dN^s@RGnXNGfi1amEQ$AC-fADSONHg^$*)8RgM(uB^)wR~7RR399YiO4|8 z9kRC*RyDucdB_iX4@wxH#bV(@sY%|M-$q@>$>w5QbbMkV2|9n3ey@Tu_C`8^4$*3I3ttr{hEu^E5 zGt1B=QTc?}Rv##29z3Z*)`jAvwwbpfGD?MUyHW&sDRowW813)6TAY{y-{$9E>L;1X1`22XT7t+cq1jJP#* z$DZwGerGJmWUkgQeZGygsS_!$d+z4|AU;A<@0O4Mwua+V@}Lon!Bl9va>52Pl-hyR z50(iQ4S-XG;PA$Lpue$E%QB#JBSEyd!sOXnio@$)>)wt+J<$Ud#`@+r*Kda2EVG?9 ziK;EX$J%<-+jlrcNA(b9u+6s5QZDDSyQRt^ac2EnV%V<{xbg^1h&q&n-7Tw$eZV`* z_h7*mcPUhZC-Y=)YMm(6-dJ#JExEozv}J_srKK3nsz5r|H~=-d>U^~)V4QFS!485- zu_a1bVhT{keYY1i>jwFvBlu~J2hMfImL#uQeX1wc3gyy13IBa~Pfm3$bXjb?3FbdZ zVb#&1ZAby|MvB4JxDwAS@NpKvj=va?t!%yShjX<-h1W%|M#|VgY?-ZT+~GEH2l(nn zRrYVU(jJ;yQW}RFt!n_oPd92h{S$S$QWoZy$ijR`Tp`xcmg!QZGSeuSy;;P?>b7R{ z_|(AR<<|>CJ-QMag&(*17@9^)CtoV3y;a5k*3&bOASQoQ@{Ww?$2x`?UB-gTNw`Ge zEae*VgiSUjGh+n*9U|5_c*g4TUIO9vhbu$JSHv+0mEN2mh#!z2sERupuDu)V(YDYS zoMPcEwGz(3HbIRJ) zyaScMSHO$)+Tq_*t!`Xlz@U`(`q{hj?z^zobsgQj?=+mbA^U)LyYqO+0(TJ)3it}W z1AVTgdRFFjeE#UfLtuZu_}H+YoW-Xna?W58T1#3$RdOG_AaXZXMuvB3yQoF@?n{N3 zZs*0ra<}#q(!oey8_+K-)HYxZB81693Ah-~$(Buae=0~m;auLT&)_XEjEPhKT)3tD zemG#!tok%Y+~&_*tQ3hp&RH7f=My_2{Tdj6hb!Sre8v)fex4j1%5`rVz-N*e`}mLF zHxk&W1-0KPU;`5XK>EL*0xW+$0v^BYu>Zq8*;d|^K=MDWv+z-GNI_+q>|H-54oT=- zvJHsJ38#%JB3oEn(Zb)!M1%zY-mUXCd!pq|*bXm2N-*p6?)3lS#5zg}Rw05~GH`O3 z1^L|w1ThcqidkZmNM{5UsU?P}*9Clk{B8ttbNf3!9=PFC?A`WIvPE;js3w%i9|`Ua zVt?dxJa_>9MWXFh%Nuaz2<3+PAeQjB1gaVE&Z88IkC_Hgt34!DRn?uhjIrsIhr73v z7sgJ62oG*cQobrQB}xICU<#yQ36?oz!qFo*tbFe_zkWeIVzNuV#w52zjY^R$}VOQf6X zp%ey625ud>E${H<2&WI%LPg2?2^Uz03-~0x;V4T{30!hRZHbvuE5c!4)n!%=w&68! zl5L>e+Hwlx=Klgq8#JGPG{~7pR_mGp4`ky!aVPi_zmaq{2#IxRD*qaIO3 z9dFM@#P0bYCGO4a!DgB^zC=uNNG(e!!(z#dDZZvEH-7e#;kT1=o@@_asi!#SLViO& zTjrW$is3e1^wY~+phBe^*U9JaYC7i>Y}E(gi)xVf;+kS}(j5Et{&R5U6t-j^Dk}gV zQWuutjn2kTI~TKBXCaD=gX~QqjW8B(FJnipU2CTi0|+5S8oeT%Of8u=rGhresin{j z;S%F+^kI4f$Pr%7iG-nsI0FvMo~|#S_#=T#DwsxsLoFNOIc_wb_4zMrQ1*T0z|y&s zWL+)ZBWnBu$U9Hw@!Cl=e4l?ZU7AkPcAd=rMoouF<36ONV%kbH)qxU|DL%T|SV?Sh z$ulHOu?VqS@d4s1oJCv*Lgu81_-z6a&WY0Cc}{d>;u9fg%7Xit2PD-nF`1JHx1vBq zNG_fdq#P?FA;@4@X(>1jo%4jyK08mOT&*)(Q6R?9!PFOcXmoT?e)Ae3DVu1WBPIMB zVBT(Bf9(PpYxsl`^IAjs*}}UMu0m55^89mMv`%yPys)3 zUjJgnD5LqzG%Y$H&z!KNjI_`tJM~g7wygSegk3H$VY9Im!DReT78k7IUAqiOU6Vc+ zwsXZzfnhp}$#ike4+^-7_9|${;zW7F+nI=|w#J^CAgNk*% z$7_Wip(`fw9IXmESBb7-pS#GY&Y~By4tq;Q#|~@q; zT!)9=EuMC>&d=Xo@`&{fm)n?{;kUl546or=%q`y}$^T{6KCO+XsI5f43+WmmcoP&J zOYyfZucSvgrJIco1y#-3k4%tdYzEu0q0*flEqqFh5pgc7&n)th_Hb>{uBwjz5OdnM zE5{_^k2%Gv#*R~IVhC}0Dx{v2mBIOVw^!wpUT^u`WexkMV^@~VU#_A~TXU3I5Hb^q z5<}FZXkaoe)q&E54g=($*LPfseJG?qnhk+SyD$g7M`eNIK*AWuIyDm;?V29|-AY`0 z$a)vr`G=HYNp_=UgtI6iMi>1!&S$at8*@}v{Rn9%mdpZ*@4{&6N1$tl8rBw!KN0*j zs$>iI3YA*Y{9_h1FGs#6=Ux_WrvC`uHWc#u!L5d~&eOl#Ce+E11SKI__o|69$e0^? zYfNz@r-uNb`aHK)YSugV%5wBab2KIs30y8G<%1}QQDKdMl46ecym9nP*4c2!BNajX zA+uoah{de6!RN}cqc@P~O;+m~Y&}GU{#}EdUZdVgrtU*sexRY~f@`C@O-heBaIUUB zCMvnJ#*VKmV;c0d^Hf)_r>maUo{zdXA+(3~YiSFQ*fVqgYzETOw-ByZFOCsM-tDZX z34Hkz#ParBT3@@Hb*3a8}a9}ujs^DWBIO9hPWg&TS#JRJT^u34vCE)}-Whf-Q^{YFkRou~jx-p16>?msAI*SOZon_Ms6{-I+x2}gET?OzbW>pN*P44XoyLiXluil}tn>hG=k-Hj6cN319FzA8EU4;%1;3Td`Z1wk<3qL$V z2IAYxfU(d1<{%I?LFjP30bRpE@)ELycYy94D4q8rLwE}A-*xZkpiWt$((CqgkERb9 zun6w<4T0Rl-Gx)U=1Ro71pv6nxS0iV8rurG?kG9;;%mCbqOYyrV0hnv$zeuzOc3{n z3WPE!OnQ;9Xv|XjcnKVv^NE3ma1ugv&V-1t=nDg;_;!4_NT?f;)@t5JS&C5QY3qOb#=`+?{gLj3cZCN=ZF~t$lA%rdq`|Q${$voK){i2w8aZ`#GpT2 z`m-Uk=swxti*H!e2>+<;LcoBDxGrFzoKmzS$Cl(0iQYdoL~aJSKtIL@W+c-wNJ@De zCUjI7{61vV7{Qz8`;F>>JpV)29RcU{BhvV{+E*&`7W5v^ep#ApIEFt!uCpBs66C#? zeaS0@T~2Iq!}wQprg{=wrth#OqtsQswc1j zW-0nvyn8!lX(DldX--$ra;rJaGKlqv&Qw7NzjHwz9&JLcAIC5{0si09Dj*8Tn@bK< zuN+#lRTit=$yY@`BfOf;Pxg6_?VC#^8ZUdXH^{Y@R$qOP$yQwlG`+As`XG2c9$55y z(7fOXkX3%Sk2-?xPllGmMf4{1g1s=rKc(hY29Nb%HZbuvtIrOD>)+h6oUqDbm!Fhh z&R~zTxhT(=Fc|^fNTP-=c=Cz@U$1~NnhMMpP{J|EoR*fKXy?iPC=6G93^E=-J4$j! zs}qa_+-&Koe2us?+Oad?&%C421; zvfBH1t^VfpnF-pwP??lj;-RDIQHQh+Uf{R{9C;go&;SAlkcM!4>M~F1%Wst0`9;HT zO|B(k=8ql~1{@rl8;~+>3Swq0;&mzN-awj17}irvrT|T2`%SKzFX*v@JvZ(2Q{wBjm_oRf4p4M7r(Q(wAYddIGg zSpR7CACU}TAHm(b&14g+!3^3NNngr|-GHHCbUaZ8_oz`bU0$#iPbCN!{$Y}!I??M`K(b=2TRcS-jNmrIFH-!d=#J)UQ6V|5d@mr00ZVu8 zY{i*E*)~`^eo7e5Xtt+Baw&xm^hOwKPbhQP(*5zFLmsARd<}&3oWRx)2zT7#d^R_A~e>e7r|w#=7xUhz@cQ) z8a~M#9cA?5siu!!D1Is421Z5%GuGc}ubz>sM%DO5$4R)${AGV+IUaHILGkLpYmOM# zIL~qM-gzIC`9hmG)vtlLw6FFv&oroZSvEiOMh} zR;XCe-MQ?&Scz5KN08ruHwb?7YYkyo{XNYo)dvpDt6+c>8h#7=DWj)o_}>z=rYPo_ z3IjvcTIPHeii+{d8N`gMvLE^*7%^ej7L5X^2;G_lZHC{@ZkMJs5iqr3YRq;SqGm@@ zi7277VMh#t@rg;>x+o8A_L@OV1fw_^1?*(A-QVzP(`q`CmP-=g!%e*Lq-;k~F}~hH z7#R+`!!Ysl7{H9V(U-WMv=1}DuQNmUN-p)7!yxm~ge$POjYE&&&uJ5K)~qfr=gT)tWm-J zP*TL;l_vR#+(kSPXY`slp2?PI*C0Utvpf-;XC70oOfdgUVGnTh!c1$L@h1P`$aT;; zH^(FkDurGuL`QhGrJPO!35QiC$Wcr}pn$O0hfRA^o}T z1ky5`At*{2p*ZCsa#ta}ALH`DX|Mh}UvT!W2Gw9?+tc3u&&Tci;^^_XZ#PFr19USH z-d|J}mY`zaQ)H8$?#z=_5AGB$XwP2=+$Y;a&gG9kAD$6qQc1pFl_778JfBU zsHH1#ioD2<$EZ&zeV7L`B@Ud%2~5)wkDc$By1V;#?XNk_Jji_Inp9}pd?VqBv8`P) z(t9IzL*k~}Ml8;+B#0EC5SSt5F%J2!#)9(!0viG+u+aep{)#6^jm8AyVFh|ZSQ==q z;PxX>G!Sxn(TO!=yryT!ZCI;LGRffdplakkynOn zIQHbT)^4>qTRcz$F@z-~P|m#%J(5sI-JyH`K%e)M8N~$#p^^wF&$HTmS(2nkydV*Y z1udqJUW?uCBw{zn%$lL&=%}GYf(R@r?jlCMRepq%6zaN8fCoq2DjzZCT914bMtiNG zy*}QxqG%dzCA-LQp+1!O+3@h0B*U+7t(`)Yru|SkAi0U=mf#TrpU9gh8~~PY!D0~x zP0m0`6^Hc-snya8JOvjq#Jlh8^!!@)c1oq2WesM@T5aEk^krUd2tXXxBIL;fwn=cv7N<#{@ZCpgl&=ve?xRUm%Dr-n_p zqf#8Jm9{~rI%W6Tdz+dS9ezO8WaD9Qqg12s259yuBIHwYhPZxrz&`AkhU9i?-4%0t zZ|;*L{VAxsf%XtNgXua`?kuKzu*!8m#5$SeBTr*~k_Ne`I%C#>-&#BKt|yY%gOb`g zxN<+`ws*yLG(qE_yl6`a`~5E&eV_fnh5P%|FTZ!QqSwOXTDF^DX9VagSZ7e`Z9hzqU(ISl{_uEr z4f!b9`eMxkh&30~!g$C!3cTBfw5Vm4OSDF{c#4th6@S|OxUcr#YtMv#4z~Y4J0EIq zaJ)mjc=3at6?T!F!*eO$c*A&c(`owk!}->kGW=!tU2mQlyg2hE#0#{)oxFIjwnHt< zp8jdi&0jO4ZsXgB@;6-SFT$OTc^>xSry*SAOQc?}xo45mf-&zo-EP&>fSMll{U@i! zkwu5>9C+03$CA1)TA~)7FI4s2w^yoh9}YU-p6kah+D#C)Gy_p`6YOf{y7QOIx+Lr47fcqO>_TEPzJ*IbWxQyE&>J*tLGX_3kmPb#Xo7E^< zF)HDfKN}YFuI{OF0$q3G>ZL#Q_-^-X5|C6r>cCGse?3{<5pxr9YhZ1|^VnM@>48i# zQ7>q?R<7&RGBLP?maB-v#<|L)Xe-D$ZqX(GW8 ze3G`4n!qKS?Rye!?B}l#jM0lkI{$dk8kOMLw9`stsp8d3{#$#2JhOpxSeeN zSz<{8FAit^gXfgHwGsNI|M>#)d7#ZRx2*)98QS{CXOho5PiH&B`(FI4!@e8aiVsO% zJVU=aAs1B7bA-Q4Ef-|W9_J8XBo}`u#7Z_n#WT=s-^RVWo!DUzvoBQ0F0%x$Z=fh# zG!0J=WBV}N_C@T$E|VKx!zI>z3ZfuOns<&S7sw$7Eb}yb{~66gJ^Y%(@MzGZBU(W! zA<<_n9(OnNfewfdGT)g$msymVh zQI!Zfq4=8k4rF5VOa7r1t7w{o%|iJ*#l?6OSa`HpJQa&9>s!`^&${$B7A)!Y@xI#7 z*)?692D6!$H&-fA2@R}VZP**>sK%cI0Da#eV$}43{Twy>%g^#fkxInXLlSvb0dWOw zpB>aDDRS(bEj?1BMmX~b_RN0gh$UjOm~^x*G0n;}B@L@nN9*%Cw8V|jR#!t7u&(vu zfq~ZYa9Bjke+{oFCrxk7Kyq5IqqE>)!S$L&UUQYDg~^rHb-NCeFB968wnQDM49rS* z6@w|#OK#`xF#1wr0CPn}a!G3x%W9B0QVaE6PR2qCf0h`&@37it*w%Ai7M1UTO1a0L z?;uyJr#l^TqumTwaZf=+X~Y+bE+a#HI|Vrwygd znk!_+Pm(Mi8}w35bMH;GSyX0QE$0L=36H)8AXo*DAHsjt9!QMCigBH8wG(p^DW~Md zWUt1RH!Ut2ger0MgcY6q+Yzs4l;hJUo5wKN_vb%uV9pdV+3J2}x=K(00RKPf4gZgQ`TyL({AMO{)O3G6x+uP< z>Kfa^dJNs}mCe=pXceFppt8%&;D=iQ)T42=*J&0P{=2R~*cTIML%4W0sXz?IvjUwE-&U%f}I_X)5Yi%=c3FlP~qYp8$(62CB7K_sd{Zr2XTKM%67 zvPD$AclJks6j^v`&C_YTO8}bo`2npB+iz#C1OoTTFK6k5E=`2k9pu1N*;x!9MY zTL^iZjsIo=ZxMNon9{^U9qm0`!q=ZhMR~!*5x5o|kek1ilfG$0%UhC;;NZwq{)wIe z&;jmXC?--t&FvNN$90Dd2EfL(Fpa~hz&_R%V-h#kf5T!yXf16k?heZT-dqRtJmH< z>gHq4#&lA$5^GlEVND*6_O2XzcCExf$@ zy9BL}IIga`>f6CRb-jiQYW8Mt(&|sA0(<->3UX@kZVSs>fk?nu9ax^?fbcWK);b|W z*yS6C4|(ftwOQ(uJPDCC6GXYS)EkWVR$2xC8J<)n2Jh(--`HT*yDDLl2IddrYsp&4 z(&C7O%5+r>%^L&7&#{TjN|>mY{S_<>-73(=z3Jy7DxP@Cuqh=%NV;c)t0l+cC}g(; zir597IR6pE4klui&2k;){5oI(obQQn1YRQU zEFp}eqhgEWvBBoUetlzXf6o@~boBc`T~I#V$5Ern{2Uhwg0Af>3e(US9K?N)*q_Gy zXqY7B91%smpp&a*FWH%A50YP~!_L&D+esOI#y0GVH z4hCUz*R%{u4XA|`wp}QK?V#-I891e z!vZ$a;y6yJsBFz27p6}DaWJP1couY+efd_&>H}VKet?W0!0QOBx9Ag|rp0P^%9jJC z0_AJeY_--x@@B`z`;y5as zo}`sVWqQhFoZyfP<-Odf8UIi6@{YlER@p7Pp#PCtwZ6rri_0$M<=xheiN;9}DJ_;; z6dGkA>vTF&8D)Ja#uzfiV(jDo|a39}p9xg)*+L*|2#&L?e z^uO+6@1^q)8n1C{=|x7dQwj~mEBEq$!S5u>QDlHRGnbj_n~7?$xNCRf>CuQj!elH4 zVsS`tr~<=_^20S#{K7qF-jJ$vd)!cRIxojETLX^Yk==nW>$P=ZBz5DP ztjz*fM1=Pe-hgF#2ANf^xbyqF-b|#6A`QI74l2r|9Wuz}98g>xQACNQe+hZ=yhCbKA5LIz~{FkoVi7JZ+|!hOm)mFqHJ)z;xCoJ_2|v5P7-y z_JmmOPCsw>dZWISeGkRuE$~j#G2HkJWd;e#KG-mhfIecw#7I2&C;0-VDnkCD&uZ#; z6NITnP|ZAj7;poPfi8r}9FS!)s7Kc2|At&u>ofB^fKvJ~7*b4mVs^Fih{QUq{#^UMI_}5hehboqS(uaK`Jp*kY8W!*~$W1Y2|DK25G=)=k@ zV`k$i^VzAeQ`0@@=O2lARZ7G8O zI54Wt51m+7QlB_+V2;T@+~ST}@@X_3F__mdDu>{D|eAN~Ee}e{poUuv{bWGkReD z+@5s`3m5B9Ee|Jtc)6dTz!tEO? zr-OpuZVMpFe~I4t^(s2pn9@4i7&=?fI+>c8{$`_%P5*a_(fmd4Ao#u3X;d*IW)_@B z;y)Ou%4TW6XO4_&+z5*+;)aBfh;;)6H1ab2c%y^MCpK;uV{IY@^7d|jxdD16I6~>v z_ag_-4}>wa_dH{e2KB-l@kgXHfJs*a!PV&n`nYRP8@@B_Qc2i>!vYyL|z>&`$72v>h_MK3I@GaPk>pNaCQ|+FPhV;K9gY1jq4EIt9IL0_eiZ5c zO8_Y;M7`yTUSiBrxxLgH=MU;)O?sueM-J%pn#%Qr1)LkR-e32*#8R~}ZWfk=r}BD- z8wjs>V8_-}JKW>``Bs9wO;!^^)FgLlvBK!TWktW8E4o~hsUmy_@Ulck>RXycr|rf~ zE>m;IrOfH7>j#HH;k|4K`%vay8#U>!Ms3Zin2xnF1qOWp?Y}Lozq>d)2ZicOQoD?MN4m2`yH`19%O;p6qAmy_G7@ui+jxf@VZ zj`5kSk!#-O^C{zFWP1{i(1Q%{E^MRkMi%|^kZV~gI2u$lMyg|0rJ1@MQQn!0D&Isa zIX+QHXApE$){>27tJJ4IbmXs;QusfQ?{CchU)CML@M%Er6!-i_25fY(_wH;SI5V`b zEj4^NukQ@Np?zf>pCtKKIFp3`3L3TzjMIlZ9XMsConC)Wc%u*&P^}03q)V>e?(3wqfM?8#2UA0S8;Z8)_um%nm#Lxr-=t;#7sF;~XJ_wX_`etp z=il<E>`0ocK!}A*d6I}fqQkS0x3wwGl$+RlU_CzN z(nOG}>m=0mk-cHwOn)BD@<+NmzH&#Kl$(!X-lvITN#>U5p(V%)tlZtE%Dd4+biXBe zT&kNiXe1EDt|yhzygI=#EVIdkq#(7Yhn9dn$k14xFNimPv4 zV_@YbW}?>+Fv!HLXoIF9F>f4GZtT$8osyK4img?-HRJYLU3OEDyi^a=0)0{X0JtS) z&CspW-|jC00z~?nXb+OC336^N7A3-c*(S#{I0%Q*y!{qyKx2c-*7C`h1cZk#u148I z${J$YPHQBm*KP3Y%d&qcx5P*3~HQT|N26B zW=RUb0E#WO7SEbN$n-sj{EaSzZU$TdSIP%qV*f9Zt>Bl)M&K7h5_$wlF`~vz^DRE$ zd=fk85cNphC+hb}%iv;3g%lc2olG0kp&-x9G=s9|m_O2r#b9ovHcCh64nqUj+!b|7 zH;E*+$VeIdCxVW25`HcN9%~c|OxCY;MQcH%9^hHsJB@ALTOsAHkrpm;Vm_r$$riAu z=aZxh7*V#jNa*f!$(sLqBETb+0rl(5{A~7RO?brE2@27wcdBw-;c`IzM*S3)U~3u6 zN?GUuC{*C#t=%-$Ot%Uvwe9SJ)%Mv0o@Ul_ilN?~vYQ`YhE&QzAPN%*p*VNV=Z>ce zSUWhPQN(js5PJE~5}u;1;;6X7Iws17&G=3b@7Y>#CLC%?T9`$yd3^A|dG@DgJtfKY z(}GAL5Z@xc+8N=bj6X{e0;x`VgfU~m@_k}^)$u>Dz;QBpeF(vp?AU-4m!Xu*UYHOo zUg%)9gVc#AeGrkRd_h1%4Yo*BtXh1dG^ySrn$>sK(bs@t&8bS9sv#kfd?g6zY)ium z@J84ursKi`1Mm! zJzwQN9IXPj9Wzk8FcT`Mj@msvQrBp;QUhGH{L0gQ(cq+=Nu$jkxSOMyTN>u3q(~oqRIeAVIc5g}&(wv6prgPZ=^z+h1`}*Fnyiac7!S z40ZB^5k%ISWpmS#gS3UY!*tg}N-fDK53(X?WAk5Geepf`TDbWZhe)3&sVjFBat<5I zmoe4Mxp`!qs$~veJ3&=%rb9ulk(yAJ%3wawNwZiaLP+&)IVac*QZt5K^)-Is{3l8N zbHld(+7FWUv8Ixm4Ta{sR{){4f>S0Bq>F#_(C_BCnI*~xCtOU!i{Ob5x8d&7jyZ5Z zzmj$`M)IjAT!O+)Q7e#F>7Va@0<7iLVylU-ILCGv-p4d9(8@}wJU6p>@2U~h*Z-6d zV9?;eYwWUcJ4AALakqBRMd zaFHg7R;3)ujv|9$g|ML~gu%!Q+(1; z2Rnj4o7Nl^Yc3{=snpk_wJ0uZd8(0CrU$+4}$**5r~tsx$0etYp? zgPU}F@b+fjB1Tn@5x=Oli2s!sk)E0K+IoETtdG%Ss!plx3Jvr6crZ1NOFz1f615KW zQq;nQKFl7yz`7R@refb%kXczK=SV7m`g^+I*`|5!y zKn0-GuJnPSvRItOU5ceendM$P7+m1C(w04gI$R$7YON1(71{Si`%};r6uewZ3=Zg0 z!kIup+@S`yEUS|xs2ib8hxx!F1r>o)V!P{JY?u?eVI&pqRMiFI94#OPu*}67Pz2^J z_Rw|q{84HzBPT`Uf}HO|8{tGIZO%{2HRJ zrK|s3Rjvr5r3xpJ*(R&!8z}0uGE>>je}G~81b(g|ZX0(ouoG|5?RI;W7>7C|&CZNY z+(V5rgl#kU$K$6RTfr}~5R}wDpQXF(ocFO7zR@*5oYP6y-y0d{)Ol^JJ`c~ZZ#}vM zaDxJuri9w#Zxp?22`bg>xD0PLC$>gV11Gk-=xd3LwmMwx{`x|xiw!JWV^A9JJa~wG zDe}K%VcgEp4_&cz**_VE$>8;Ll`7{ltUc@JS&TbOPjd1jzB}eBZ=34e;Le_GpVPJ{ zrOgH%^Tlv6|1=7kGW#`Nm{GdY2|jlE|bDd7f@i!qe#$Z+^(3m z3RrHX2K{?*GTiTO@${N%9Kbq(Y$yuiKR`Y7n6D;OtBFJW zOYTdrs^oCqJ>SyR$DJ>4KP{_wg>D31aOov>G%t(0K2)Ouc?@MpRl)yk9DVXrFJet{ zlzzktw(L~+rBPlpub%Zy7*2Z*`HBAI&FAtpv3`*->fZeL%&m+qAIrORnKJ4Td%VOe ze{u?b^}D=j6}Z)YdqbwD6iqAChqu#qqCbld)T7mWAJ8cH z9J$?Ui104%cx9sVTfrmnd0w1AVtgP#Z}0qG@>ExJzkdLyMlY#ePD!=>j2w?74<`LY z;jxCCVW8if4u)S~cgRx%nmGNx5oD4?`BOm=U-Ttx4H?$Zg`u*+A@;CG-cQZ`fklFIYZzqm9E(DO&oGw zS2rBf;^6@e>47F29hKWmG!ypdU~QizDFShJ=h}QG)&a_#WReuLA2ms?c8^rc6_4J6 zJSOb88a16nhJkQY8zx|l^`@-sc&=@!c{6h8G{(aRsRQ9JnN3mOKQWvf(kJLOQSnyj zeyDQ?Cv>LYX!+SuOMaKr&CUJ}Ac{y~bW@n%;)!%9a4jU)oW&d)SR&)hX3?C=LP^PG& zF#F>#Wn<7sL(}YZf`gn&rMQD9gtEv7!prVpsM{3yhpvpA70R944;rW0gLzD7ib&A` zqLt9vA?dyaoMeZnfME;!BdP8T#ZT6qz;UPYMd+9mW8K92n#h{AiPgPYmF|+HS9D|+ z&5Miiu&1Lv%c5ycaa+c+oqcx*K8?}LE%C^?x3U)(1g-s9fzYw){=M7h;vM2RFBv{* ze@I0E+dAvA0t#_2PTg$eqLh-4m5A1Qx5|}fCO`Q^HB8q?vhO1^(dfh-C*wluezZt% zPbF4D8Ejzic4+Z#{dI??Fm9zW#UX505gAX+z`+aS-_05{@$!|GLCZ415swV*HUw+W z$%zUp|Lg?Muym?pJ5s9Y0`jnSJtN{1=i<^(RcYkyp)+m8jQ}~h+S*$_-R#_L-5+O9 zH&)J9OOjyEfO(hfn>i?4JA1{NN{|EILv-gW8PefixnAj58&&FJr0x)Bk|8kFNum6` z#b%M0H-x?GMaGoc5_F{az$ZK>Bww+%0`mumofGUCJy4dz@Y$0BtfL*7g})w>Bw@4tk^BU*qNZ zO}5q{d)NKiM;$60TE${MwlGN3$K!-fL?RCOl=jUB z(!LW=1*Xd5ggJdY3bl1k(KliC!A72mRx5V?_&b!Z1JBE?rN^YObA#}hOAFTgAp=2! z=+_de@R;sKc-2cD61V(I!J@oEIFMmicz1>&aqB*oDnirZRN`x!*2T5>9_Enc@9mHj zv0yb2Htz2ylm=^n3<{c}_FV$-|d5JZt zA7{cEYCIj>T8*)kye3Qoixw;1=h)L@cGuf7jKfPfe%=gOHOO;a-DT#p)=>*i@RU1IBj zB<|kB-}n1*>>q#XxH_X|(i{K(8&P5aeT;7PKZC>XtNGsy5dU8$ga5gP`JbvX=)eF} zkN^VMUY9(hO*)X~t)zVdiJB-PN2gA0F=@J07(5eALwdlWbbNc@{412v9SLiE6egP(;vI-zRM4^*ky`4os-P2 zPX&i9)tIwr#4Co9eNmAWWeiwSV{{0$U(yKm`}GE>on1mvws+~U!L$FyXcvA9_c#i% zgf#EGXnz1W)r2Wi^4BLmyO>c1%|sNa=Ont*P59=&v;jX}*O#>C^pQI4V&XmqPm*zZ zF5{dCRw%FL>jdw=8+!B}(2vhYKJdlOgcU21J7P1Vj1~@}KK3;yK!DGv%&g&HdoH+ANl-`bk29q3=X+F5%Gdy*}IL~D63p9$8tfV7Am~^gFYU1kb&rO2e*sx_g z)hCBT?Metob^fipn+?=xBV=&PZl$ob)z=j&J_Mow1ARzUS_8#b&pC~J-C&Px0*}m( zl5LxMOVRo_?n953Z;m899K4YdWsj8JPKt|S8+Z^c>0Xtr7|+_{Y(m8r%M$8C9il7?xGmkVZQHhy zVcWKC8yU8lVcWKC+qU)M)~IUSe~j0tx~6396=map%WSl2Eok z6qN?n;B;~oeZ=3YQ4~xtO}&|dNelx)eH$&{hqP_FObkv5VPccXyN*dDIQBzXwI!Gn zvtuZ8K%a4lkeWh;x$02cH5$@*9IV`^kQ2_v7Qno(6)a0Qy!tZQmBa!f_!t^5xMJ{w4Z|c*r1X!MqG@WTn;|+#(rnZf@WQ^lECUxixb9(!TG`Z{LRt92w-wxeGMn zXAxyl(-@dvOCTXC!3=zW5dd&TckW7oTWp=9ndS5dY_%wYsWJteax94fUL+up{qaZ4 zgnwI60uFawdhEMkJQH;x;uV1fkb^R4Ed~~<#=^RYTD^S0b#gi!nb7t*1^*SCU>Q(9 zB9Cg7@Ky@&fEqlt0-9W`C4kUa$MwmZA&S?W28!@xlX8;0?n1}_lLa3pMm%bH4Jxb- z(zkHwP+6Eg0c2hCFoAB^LpZD|%uUH_TQXI;|YJM3sdS}*hlV^eq z$hQ0o9%>C-O7~MfwPKTg!u19)c{Ax7U%=1uq{|bXXC;lk#2L9zQn{&BQ=I>pgiLwX zZu(SmzXxpGxBr6atV>xF<&fXUYihF=A!e+jjKy8zsqy*!@p_+6R30C_VKA|$wY@D9 zPOZn~{QD5_`8yG!QU(iP(tVGa=>V zAk{%!Y#ldZv+v>=UDNd9oxcBLPr3N$kiV%n03}~|if6KSP!AcL0}$@0amE8kxEzAX zeN+Ci;^dS~mw=6lH}$67^sCA={rpWNWW4?et4ax~K8i?|EH>-G< z`)$}2KZ%poPAz=71BK3Umk08}27rnTh#G*C={v0W`)nE&GL{e1L{TsSUZnQ)s#uJU z>6r=Il8YMtDox%z*B}^$I#5%53GCASpctV{;vZ5+O|Ds>q#QYc{Uv~YX(-ZTPkUHN z{LD$VUCAS9*zd=$j-Jo9D<-E53e^{^7Can0*TM3Q#*Xl8`%J&b5-5^CI<`s+mX~f)JGe=?f@G(@`!U{}K>a zo_PX)V`74oWfb!ic3B=s)zvDbB*3hifVLh3f|+tcQA;4LT03v%yVaQ4*^ymVl~&9MBqM2jyok(I-PHDVYj0tgL? zE9m}T7UKs5=(rSQlbPVuoN6Q(fT^iHK0uHL@>k@@?p7NUV6LGoa_kZ!&S&f#$GHb< zp{Z0eP~;9vn$LA@i~$%M;*;&aZ*pqvW+V7!39U$FaW*!mDYmp~x378-H=r~7*R~VP zbj|{y-z70OH{}-Vm+YVEbc?j;C`4lT0ouOeW}|JO)-yV<6muLW>|8Zgo3zd~km|kd zXL!vlf3iGkZBAL7Qs`fMlKqQgORD;^!{xb{6z=)r5P(my269-Jo3;QMww9+1!v%HG zB16Z?L*NnHVNwihKWuE!x%FP-yXYs3?IjjUgK=%~c0olGbD^s%q*IhcokB~{A&20K z*C@(al=MJvOuEn^rvoW+i_dfeNmh%ln+SU^bLOMf9u<`_gN5(tlKk5d79EDtm2$ab zZK_2|ooq@cVt?L0(GB%^rCKyNuCG)Q?;O<1L{(AWVa}M3FpegFDJ$EDhnm4w;7yY6 zj_0;j2g`YE!o`i^ZRJBZP`WHym}MMnp8gtuSwhRIRlmBq1Zo~xN}pUt)*tb~w2(08 z9(63+z3jL~P)$x1-WfHx=JSWA&uPs%dX)Z6V6+yj9(uy~;V;1?V)IP4KTR%nus*0S zDz=#cQQ5L}ZZ8p}*D7UQKNx=NhI?~UsyT#)Yr|Yxn8NB%IaSz-;i;Q&bfH^Evq(T*VAnr0}@R|wx1DcXJQ8oX?Sk-tVsS09YGIe#A&)i*DbhN;&J2Txc&PUO*nGU zT@(qvh4%`)5lCiTjaxft@wR-E1v=ucXItbRZ^oUgs{lKo)js9%%SBw_66%W`A*8Ao z{nxd>%2b<(krPI$z$&pfe*sYx9p5-jeBO+h&?KWL{4VEkT!z&LaU7_`r3E&`v)CE% zl-}X*JvTbkHPDQ*U@5gS$E($2s0`zFt8uHjassWL<^jY_T3yaerMC|5;|3NA+4RqJ zSPMrAJHO*2F7H2!H+u^(xCv@ZO_MsM1uNvDw92; z8*O&SoRc;~Ev`|HY+>(b2v)Xg$hkq_fww6T5_!kM`P$S!zLgP1{?8H>R*jNOu5aj8 zX*&u1T#U~RTl=@#0VM=klKXY&tQLpNs~-asqyODCnBEx?hc4Nk$+(+*=w__Zs=f3; z^NL)?rk`_hjisfPKK6COR(pinebuayJp~k&!Ii=)mjcWnPX>nPpNeK21O)o=+W>(# zCH9@IYf(xr52VGhMV3v?Ys+>Z21Kp0E>hPkyy?Vi-E3ZqVjv$sZn zY;s43;_rY#QJt#SR~WJdu+yJOTC{=`X1Ny52j>da_@~@8tOOArZDr~qV9{qX8A{$$A zH*Zj;l6;RaSDfp5H(SONXxATcgwShvj-(`yJh2^4wI^F>F)~x2d?$hOjQOIF7Qo>M zSBB%FsKP2oy)_CjgfV@o4CDz}K>JePZE!%R=!s)1$|ahG1qpk4IVer z@3zq0F)bHbhMX23RQndArV14HggK^!5*}1T^=mRiU8~nr%q&wi>F04z(t8dl$Ok0@ zcaq1rws<{;h*S~b2Y6*Z4-)Plo63-DSmz@*bMIY=b4phZ(k`HiZVmM#e^fwMEN$iX z_`^Yc$y%n`riub*>UKH>W^7;W;z50c>LskMCwTN54dMv?PEAM%YpRj(7xA>XS#_8UZ%61 zsct(2N4pskFyvQi(rU0}zNhbad#PzwPFprdfHQ;ocCk62YZ9g{LThS!yuVp2yTfC; zsXm(zdj5O+h0%c#_WH{$7XBq)$^OsUFAGx(6JuH@3o}~-XBWr+)l^Pt@7ey>h5Am( z?lD|bqe}8>uL0nPV_UO@U8GfJ{@A`fL||~jsd_Yx2PtP6`%}m7jk-jku%v5;*Q!2y zQ`w$92lFQfbD?7`9R}q%58Oo1TB27*;>87nS)(!i0ZYV!mZNl*BO?0YnNr}-d({cu z+j}yHS)UnNO1Hb9Y#6g%i2cv-G0IdIf(B*r^7Yb;p=N!>w>T%E<3tpQo%@B_~;$ z!lK<(`HUn!E7cufJd2kRJI+Vo2~~eslzKB^-qu{$mP?&qL}J+_3uVP5-H-AkVi3jc**L8>mwD00<_YH}0O0c#U@=F-jEl z6@ykHttv4LBx>}o_mC8!D2iwBC{W%S{4a3ozuQcw!ZAW|r3BO|fyIO)#oF_e+qfjm zTlJw;R5J8FHyk1}gXUkr)qDK1fCab(XeBjI%am9nSVEXbzf~s;hAxqTQcn_aVm)9X)yM2L4mI$pANb*dx3TH;#u7R5Si8 zdTYC6xOk%YOY7tReYpj!BVGw~Ap~WO9TdIo3~fG9=t7Npm#1OGtvIwD|q9XUsgc*k(hgFxyj2K0B|t-?XDRdU4WL; zM&E&)=FrD-=4r6lP4^1F2U-+P-`~e*Nv)G^jgO<_~HhkqGSRn9s!weF}6!o@lUVGAbwl+TgeRJizp!Kf>bG}Ype79E298OO;QJaAJiI{XD&ml54|o%xLqo!eH}!R z_#Z%I*&sy#n~HtUXdqHnp2;yib84N}qOd1iZ8 z_QU&RdvCr)1-QepM>EM<`(S>>hH{vD7uC3We2gS~CR@E|WKp$${0t_SiyMz`nBYT} zZBja0BxFY ztj1RN2&E-c#y@HL+$f@#4V+Mar`8gNDe(#*^!As0Llxco->j-em4J|}N6obHPtT*h z4y@Zdy2d1iKEH^<1uzYDX8_KrLNM?!SRT6)Q?+m~B*UD>Mwmm-1VmUEkpi#`=u;RE zVR_4VWZRAG72j}MaS>w5?H`6;1ejYyMEpERy%w+Xu^T_G0B10XBkQ$M7X}74TD2>I z@!8@BUL4ZQMA+!z9UQVBmJ}oa zJbor$f_ac=faE@?yY!!k#UW~wpyxUa@ENXOv0PbnlrbDicvw&KMV@-qK795yk;I^* z8^ma&cOzR9?3J8a`@DcZ$$&ouQ>i<{duK8-nc1%h)((gk&A=Qu`)zj zJ%c2Pl>uWJV=o6yd6^TX#gSFh#0e12MMX#3V48to}tXsc3Hs}$hD2nTB zT+eAdm9T!h$}QZ@&Lb)eZN34em3cpmLXyM-Q1`Uiq~J60xjy@|BQ(-szl5{e+DOB_ z(io~62$d0l1WIo1Sd3N`UvEtW3$i=*H>!gp%3V~CTQIh*8%Dxw7c}FHC8c?=TpE>wS)#-Y{ z`Nf&tc^N-Nyn=Hwfh_@P(qZ?X7<)I#LB6?_hd8I7qAr(dkEgCj9M8K();QwZ11!D(UL)?m;VHzqL1$wIYEd79BMEAHbKgL}w`k{P;l<=!t`ImV-EQkiY) z|COo>Jy)%8ud`~s=IFc}U(k2su%yLUPEvXXaL_+nmFg$Wtt!(tOaN_tXNky~AE^-f zT%kJ>DYW0p zLdJtt9X(9)Z!AARSxexY{^cu@dLr#v{UAbeE2iVaXAw7}7BG*|m z;KaJV;`9njS(sWGk5Y#V#ko4fAo&_H&9-cVmbnq`X&0Kt>|8xPx4L5kY8vXW*nJ|* zfEor_@-0?(3#CKZP5EjiOPb7Em8bNLt<=a_pB(cce*o1OO}X+fFmY4kHEW~*`>AFw zssCllXk!-lv*^e?d?}=B&(KvbK(^Eja?WKH_aa72WTCH`2O+Zv^eKe&EGmO}hUN6p zBYDclmFU9hb=eG}Q7fkHsNy77Pmk6!<6q8{5E|8TU@)|Onl0T};v>Y?RXuZDKz#-S zM2nXyj(IwK1ahDmANbTIDY4pFWSfIOUEjy`q56qQUM_I5oNMEIcB`z&sSEP;;)dIz z7iSkJM7|susO8HZ2C8Z>Ha=(Jn$LX}s(zF2Jq1hVGDpT82QE485U{rG!+CC3oq@ESj$YG3I(=S#`J+>VG( zORjYuf-i82*yn%U~Rme$5o)0f(*p~j<{q6>b@Y^qy7RU2jRm* zuwr`6&YlxY%~5?}0d?zDGlIF}f4XF95@dbUDnzfQyw9FG$m2M|hS!rwHLc8#QBOtC z_u{)I+R>=$Q)5?UFn|(Ji*>wXy%1&0L1^2iX`}#ZcQv&YM<%&c)QoxFz&33i z#5HJZUB797gKG6rUd``#@W3xYXsQ#2gFuFm9mJLblYt^2w4f1wnR}4n6R(=7%eB9o zU+k@g86`)?=2ZCTWzap=yuv~dwo92;nf>cH{m*p7HEm-lpTX0g)Q0E8ysqF#>UibQhwmd(QA0uPdBRb_TJ^wc z+!9CN0aet|#i&zr->4`k*yx$13$V?Qk}2=uo=M(>k0<{U2z|90s!dzPi8Fte>;3v18WOo17|zO z->vr*wv`hy=e?J2$bLsZb5c40{?&LrO|6SEpn(L(Ih6>ghp1~KW~?`Hv=H6JkM9=k zl-IDUbn8($Ta%Pw+0waNi{?GSLZC(B_$&+=ICV~aWF!OXW|*Ea(wOmdOu89ektsYL z!zV7wvCs1e{v>z$H=Q8zD&}*PM*|IHv~qZfs5PUZ+U<@~?iL=yYdrI@4zJ0`$Ow%T z5o2O_n0F}grKKQ(xEP5u1Tox$c3R@ap-*`V5i2{r0bR0Gfg*hTM`wn3Es@B=i|B$=5=x+(! zsDr0C6C;Zxr^*80ZF0sX_Q}MiJ3}JM6+>cMp?p;;hv=2d^XLC4p|}9O%0EI@ z*2p+>`r)XJjl0E=#$r+wK4VIVta+Cdf~aUqyPHDQqA+n7cc{kj5#9p7YIjpN=Rvej zsC@erCIKQq3TS60l;j-i4Z_h z8}|t{AO*FF{r@0DND>eV5arVH32IejT<|25mxgahJ|ey~M}L!0IF>LAX!k5)V&0ii z^-4Cjs?0SGG~D~;?MZ)XqD6!)_B z3NpngQv==>HJP;sI))@J7B;na5{XK*I;x?!gHA;tg*N_S2E;-`x^6JAaiE%VBfeNnHnuc4u~lr=%L&H zI4=t@p}?9DbmJrYNa3JK)OpaAeDU=_r3xJnN0(d_&fL75+$f%)lLZM_)$cgmnkQ8) z`!rP+g^C-6iG1>g#D@lTlZN`$UGP?bgnYt(uZn?Gy@MZ2$_=dcU2^e()R5t)GGCCA(9V z-id++Yj2y8UU4Pd;r9lc&3ua<4pj}QBz|(v@>w_*Rb!JnOGOfp3sG^*2`CIXlm3hk z7>iS)GST39-tMl6SL%GM^XdqJGzYgsdNZW_1n65iJ!^+HcJS(BPp-1P(v>alhc2w| z^Vsb1%|1${J=tQ4Eg*Xy9y==q%P)rt3gx@uC#Qr#Stru~*|13wd{=*|60%yP*tnqT zpW?1eJXj*|E7=+7@u8QH4Q*54et#)UWDmOpT0OYzUnp@BgFlVNPA-OK`47M0H&trl zT9v4>V}Di~?+19nV;^$I$3E~|mR1RLIvvCbg&|0rGk!7)H_u_R(ohH00J12Eb@Bzto>ZjkI@_ zZM2rYhGruetv8}bR4w>C-sWYykNP%u7fvL6XkuxJn2OYc8PF|C3KK{DYu0bci}=eR z^Oqh%VliDK0zt@FS*I#xc}N1r*|;l((+EEHoUo`sv2-xZ<}4oxX_GLr8O&%hK2AT< zvdrPzfwU%*R>BH zUAF%^f7s8nbynwJewEOZNO!39jfe^oy|sRns$T~&l09yKTc9r~vs1a(?QOJRr3C@5 z$<{7qF}j@kYf1r1I!xf(bu50~Bg`5{Tw@OW6LV~gs3oX|%`Rm2HpZP+R#{>Y_w&-1uKGN zuJy0XBH^v0O|D*-5F2tf;NJUm2`!z}&*4QpZXW;9uPYNvQ8_GJSs`qet%nV~ zz_>o)vRB7x8**(zFR`mP9E+Z>MXOA{Fq*Bj^h8c2C~TF?X_7rRuG_VPTc$hCnbOmJ zTKqGZ*#5@#M0b5*9C@epzDuaVa8W7xrd8U-uA-&Vm_-V*n2ldTy~ws(NEG)r$>2Y> z-lEJZLx*^*SeR@B{p2~Tw8Z7RXw}*Po|1ZT9dqeJO-e79k;0 zV6(iWh;qlmLCX2X`={bSMB^90zazy9!?G3$Zg{GWCd(VVLe-NN>C*<~Ft^^pbT00p z9lN)L>$npFqGr?QB5YsaBVM^CaXFFpKq?il7?qyq!uBv>IKXNlBE8l)#UM7U7omf5 zBB0a+=L@UL2iq2fH;nbGhs{m}!V-E}^-JF`LR`EdYqnm`*4Jl<%p!T5Be`U;)nWP^ zVEK<6ITh2|U#Po}&59e92l(qO@+clo)6zrK0*0b)XMBUIyo$2#fjppHws$!#Gzi77 zXStFu^EJ!~l1(U0h+&pjg5B-n-XI15L^V0sCA#i-dV}?uI`g-WG4N-eT2!Kr#{};$ zJ8llWMA!e1+!4>-AHuY%++0OJApSR{)pROU=+D*k#^?R;*NI*yC(npx|g>i z{CL)!qm@q`F-cEaA$oa%FsfqqAb<)DnwsD$5`o>ACPeIeLf_`bxEkI(CAzwKvPlKN z^>-5Dc=d}Hs(~JW9u8L~Cy!n%k}jOfQ^+Qq8vRx!(-YLuoQzVcrh@~v^BXgXE!Y0{^W;O<`xP8x!L804U2A$tz2v{DpXBHI8*fF^lP?Dufv_&mbG5Yom! z{B4|?`*yO|?Ctf>{}8RG)K<>-0s{b`{1OVe{;xZ*UtzkHf!TktO6+Wlez}AjY$!f6 zH3*-xe|z(3Mq)Anu-mJ_(Ed?|lG$AV$sZ<5YY~AaNLm-Z-EvG zbsj!wphS~Y={w^_^R4Q@hFU0WU`SII%B<(M(q#&c&aF!D2eJuDlrWG#P&+R5cP?C} zT+c0LgXgPqjwi>YxaH1iSJVb|fkqZQh)DnHh{I_P zM#hfIsE4KXuZ)Ib^ws%(K690z>IUcA`Rx95Y~c~E%u^LrFLL|)Psfz*R1jn(nhv+V zzL{-BB9OX*j96M_%<5j3u+S_F8n)7@1%3_Z4%afCRiq~6hdNXkV?o~_=t_|xZurE* z%W3CXR3zkvNbRw(>bAYgZlYO>o9eUlX&#!{N_Z^0kd-E)0m3h7Qbgdh-yg3Ts5zXs zilPV1h&b{`dY@olcTNUnq&@xx?x*!5aUB&1EK_*h^+?pWFachs885cV_0*3=8z`Ay z7J5mu%twsMC@2Fhx!B`S61&R_-6dCguX`A6wPM${s`TlXOs@%u70qX1K={r^H1_7< zD4SD-srJg?UAY#s@){05V0OiyjHBi~8RhZDCg`FdUPM@}6BrWN;_$&|jz^@-}c12sLAwx|D#;g|gB{$Wz?m z5iGtM9fyy6LC7&-UG&!vJ+F9GtaK;((H9a%IW~I10y8@qNT?j_bdIC9#`AkM0aVfQyNOC2J;ZA&@ed2Oh&G)(+`&kvv8o z(>12vp2;(BEK*`-^kzT*K{xVu@0f!F0RR~Ooz7_gU*`p53nyoKM;B`oCwc>8%U|nm zTW4B3R})8TJA?nYgQ2rAuy0b+QrlpF`KI?6lD}@@8Iz@?wdW`@(sq50ORj8ew6Iub zYHs-9laRF@fwcDW9~i4M(fEFpNZHiuqe?d$u)xB(3ETa!&jJ|{_+*IqkZ4!W-QRb| z;;%Ft5Wu2Oc_7)qFbRODKXcrct1eyuEzk5o!|{V;W5asQo%)D=bIdZ1c8jL1->ra{ zcklOd;0QNsFyQLdK#HVf|-~v;3u=C)5qWQ7DT&n9Z(J~D^-V_<# zD@?9EptJj+RY<^t>p&0lA=@!Trgs0f@~cYn!Do>n5y!A+=Nn)k_i`LdS-*pUA)GteSX;s4K*88Yb9qoV}@rFniNSN$v)q=sCRO*MZ7# zN+Y0PnJwhtOG;ac2B~FrpDs&~q_SwAtW^m8#mG+gpeZGCBh4g?{{b2YrAi*Qi_)M> zf*xkZi=pm%E6!C{lzAx(_`b+L6h#&9Wh1;}U* zT&8FkpLI|cp(|Y!AD%7KUme^l9i$eqfBsr&(x{;Zl4aTregA&k(Ocbi1%^z0se-zyP{2^FDy0;sVW6t=FY6G*OIdd{8>F{us`1--d*e1T zKZvJzIParoFRsIJ;%a=u+(jz|6ZeQV#l}H_zV6ytZG2sU&e*2Jr=ixJo@cCHP5Jr_ z##XWF9y+j-t|&P*qPXM~Es0<4b-?^0BX6Uwvqi9NX7^%*&ynWXJKPRwDYl~XW4)sf z+j@qT^EfWGL!7-&_n;jr-#UP3l6=}_&**Zg`ObYO|BgQT{1nEmEd71TAT<-%t6%U7 zME@@S!xy*op18#wLVHo9uVlpc{CQpI`EC(k;dR`5HBFI$i-fj+jOMPyy3Jj34PmLP zGir)9U2N4f$RsjXe11TVz4}0hBb&iZIfgd+eLCJb@3P5ZkoiTyW=P8}0(k+d(p`?z z?n8Ui$l>mRl)k!L*!=e6KPJRD$MUKRzXWCe-%C7^|F=B!e+bG3*4Dp;%6}!CDm5Ls zjXwyV^e-8x0VID2|>NiQcpz&fHN-MhLXy^9WNS-9&-}?GeS( zxc|7=A03dlJ$Im2N^6UDsxm_EHO%Py~f;-eK5tT!- z*y~a?@2^c1qKK7wP3pJmwF2cQq0hm(B^w|77z3=NUEZY{ML|*`or`9=_gy@s>)+94 zZJUXovcLRlrBnbLBZ#LAN6u^-AY$En2=1c|YfKtdBBER^Gvp_q2kt0YB@KZ-Y=e;5 z+3HmtN{FSrlu71ILJM%}Z3~nG9MG-4%myh#s8=N1P_XUnv2gU01c$;^5sx%SVM|6z zVlMhbDFo<{zkHxTT}?L31z!v3Qo7V2GSkncWFr%z$^(v%LFz01lZC)G7BNcbe1 z;Se2Xg?djk%Gem2{ESluKWfzcxNne}*$1MfRCiOG*rA^E`_4 z`~TVQY}WFvMR-8H%2#IuQuOIjKLT?o4pB%j39Xs3@dTfhM;$4&dbL3L;WT#C@5M60 z9|NlVZaRCc9L)l@pv3z&(4xYe8Af6<4h%<|(Sd$a8{!I+m8mIlk?P@H_3GFk=l?L7 zYwfnM&!Rt*G3+#}ZUuI!T)OIu;dEfdBV%J$DuZo=&+Tr6dTyPc2AFT;YruBv?XcN6 zqLLCysKf^n5Jsm4zhnl}FrKj}l7p~qa!I_?lyEQ#yl3E)8T0qtf0vJak#SR8E=rp& z4kKL_MYv(D-kEkxuUCUN$C7PK&juy_j?~|7^jcS*e!&DI@E>-zFaP=0V~Qp;4F!;2BI7P3nDkG5@YXtb5G+R2 z)#@;a;>XQ~4@<%FD06qGwGGwLe5qDdcaH5HwdP|<;bMtC{;v<`{kSdEj}SOr*F_4L$tBlDhME# z%$5A9|GiD+Dp|fLMLt~(<}s)Zo~hfMO!=4jWh_HzmR99hK1eUbgXS$b3J-Zk4p);! z|H_`PgN`F&DO{0!8>;1C0kYbxno4;Mij$OI-XwvgeVoOkO7`FKiDJ{{P~;?03G(10 zQzVrOlHa~`s#CgATJSu@ev7oYP(OJ@T=Q$7og#&XD6XPHw2MD?QUmXEb`9hIs6ulOI-IKoJA^SEvX34JQf&zWGfb|QH%-9s=Y)E@pjnE5w0Gd=Ey!_&yOkLnMRB=(< ze4lQXt6m#bE-(MA`MziawL~zMVpo#HgZX1X!56|}qnOYhJ>C7*E5(;8}Jbn#z zJb?>6|JybhGXR6H z{_=`|@Y;sQ`}76aa;m7#&PW;e&IMf}fJT3Yk}Z;J14X?+6`aMBA_sm3n1)H57qTaV z8}k>kEPN=k4+lRK`=sVaLQY|pzVZ&zVGsCIzZ4;_W1wm=ReLWvFjtPcOx`8Q9rCCjNT#}L+|_bt z{JvrZ2E#dq7llS?!iD(;Jmz2k9#1hT2dc`si6>M-^RoQS~Lm&Md7)+39g>7q-djeoaAkS(45%|uL3m&ii1^cyBDVcc})j= zAyr`hW~7$D%M6pzET2%#<26~_e+k|(QVSqg)p@sGk zsWwS3z?d4vOVQS@%X(Rf$D2YvKHl7uL2A#Rbf5*F@N=r^0kdXykG3!zf-TDxvw@OU z;P(ys-*pcqOIH>yzZF;2FPum0|E=P3akj9gH*j)R`ThMb1^xf3xm2|5*5!Yjyc5(& zd&;6Xa(r^_LHQwhwK@y5}`bUCF3mIDgkFmyQu2TV+cInSOVQzZOH}JSJk?Jg$C0@cpI)EAuP~)ferfv!wxlC#XhU3cGP4*3rl3yb;Si}(d{%KO0prD$2Fv7A8id{p(FpDOQiiHV z8w*InJSodU53;Tk8}JP2<2Tm;9OA^GR4GYoKpBiYiZ7ic9aWKr+V$zyS^|rlH)F8r zZik_8SA0ZAUS8uW(k7yIP9};j1F7?<4`|`tj)8IozP;km3t8o<(S`B80nVfnfLdrL z?a1O`B~??&{DHvfoIwv< z^7>_y<&!KSS~{6q+L@o0El`QB%}JE&^)ET%6}|{Laaghs%Kq9;Yin4H-&ug`8Voyn zxMbpRPdA9*fvWfX` zv-%^qy`j81=cUDW8Yu2&RrkIc?51UA0H!*(xnSD60H#R*S+7zD9|y#j$>wERZ5Fa9 zkk^?yb-ZN|zd$)?{W}!$czB?@H7sw)6!pceswX}EDNbD6b>RbXHyiu>0{`!kO>&a# zQwInDfc!hx_#Y{H4BQMXoN1lSO>7Met^ZGD}i9;Zf zntM|e3W92yM`A?(TyNtwniaqjwlp+2Eo@z-y2m~cjVaD?rtY(9jN}vN3ne9)9Wluk ztZ~_Pp$bM2ZJa#3@=Zt*DLW8y&EzjxF4T;kTzU7zM7kDq(hNqTSTq{2;j+)UW)?z6 z2;c4idm1AFVau2$T6z=(fPzcWszHUBpr1+xHhBZ!y`nhh(I-z}?2m&oqL23%frAsD z{?3wZH~`72%|*~_1S6Y;;PHTjply>U#o3hLnwk1A-nBK>aolz1mkdmU~EC?J+qDav7|u4k1E zR+_A&eXAKt{nU++9HhDWodfA20V)7PoArT)LBfd7hI!Q59+NR9$gXcq02Ae3$$B4fx-KBS&we z6!bSRP+wX-`Ou))kbN91L2$gTN9NGyomGOf|OeuN*kPG zy_o%l5Ed{2DRU6cF8kxAgIe-nG3sIer!6r9$?T zMv_ZoF!k#m^6Z+Ucc;eW{uKK(ez(L}PBN^-_guj<|$|3x^fP;klE+@N$n~o}`twB#&2@olL zJ!D)vVYNJ^8=NXW0xiPpZoCKDId7(`pQAF_-h<7k+p1Ri|FQNB0g?r1nrYk4O51i; z+O}=mwr$&$wryA1w$06+ncm&*M_9QK7SK zAE`->Wx%PbovxEH!h~knRp=c=H_8>B15u;Gx}h$pFN=heui#8Pso~l=gMu|xEyqNT z_8ohoRo3Hc&@P3Y0(=PcPS@z_j)qR>Zp)|zdcBblmTpl3bMrDKB78h4X{Q4VEx-+T zkzj_Eb|S!2KZoK`iN9_Q<6d0A@S>88z8MyA0!V6!y0c+4VD4fL2z246vABB+?>EYF zO&OWyzy)}iH}C_{|?11us|;rF8f7Hjhn}=eSD4) z4uo#C14L~mOrCVxnp$-1VV;P$$k~A?JfS7)=Az=acWWolYb++hF6I3@#&}x)SQHNq zkXOh`2e`EO4BqhkqU%285-Hq8-;Q?`%~GlWivW7IzE2S)$ZUmsJP(uN86zgXvxXXA z-)?nH*oX-JP(+tA7=VieD$H==Y4&-C0bJ&s17R!v#HCT&pWhHC)C8$2at&L#MlI7Z zMP5Vp^t=iD*##;&k`iS1@i$l#>m59Z{+oB?kqQ@0nd5r4<(7Ejb08woJ!j2tf=l3p z__@&qnZCabwA~N7wwdi48-fwQWo?LGtMW5EH;2bLO258kESMmA{%8m2+VsJ#J`ONK z{b5#Wkee(6q2aYA35t#$nDOV1zH=WW zsY@fG+RRv$oGRi7p?ux^%w=v&=1im^UV1k-xPxxJMn30}o#fkLy!twc5t9G zd4!ht0%dm1PT!uz+ecQBV|V$^V(d$4Ls%JR-G;gFTNgnFb{ln6Ot=BsRG^H9khtu@ zNKQ9#`=Yv7QgFMZ3Hli+IrDq}qo5H3d%KW!bDHNSS;h%mIvpX8Fd-{U?jquQwb{Fe z81o`FY3y%s<5{u=sLM@UzX5CoVb#9hYoYzW8TCK$A*n(X7mX3wo5+fG`Pqcn4&Tx9 zMQti6&O%DzM2x`jEc!uFHs?pAKpaJ#Oc|t)Y_aC92xWN)u|N$Og~>q>uZq7}IIaD$ zpSCglzapqnw5cV3wQG6{I~hA0z=jy1h4Ica6{Lq=5ow45ilXcXTh*}@vXd7MNQIZw zWCz$_rjeYr*4SJg0FXV6_Yuy$f82cFjAw>X~Jq!@JP@ znCgW-ZwJ3iyRj%6v&1IkW|?!7SFN8t2)?Vwh7YTx+o6d=Ji4DqS8*duM*C4^+Zjjc zqA`wQxY=~3w3oqAbS=WS?4R8Fc87)VX^uR7VGwnbLcUvdU5Z|HEf6`SdG1$lWCv{` z0`d0bYf6O&_AHM@g&R4eU@BfO{^)l)J zS78L_KAX|~*OsXRAIjn2!Skxvv)QROsJf=j0-BmJVGlI9ZbhlDUsgi6>u4R#w*277Z;M z+N;t53q|%+{J_*d^X@}yUJVe^&yN7If3nMdy(>n2= zJCF3c#G|4H>#4uO@oGL}Cg*3_qUa<%b#N&+r@|RIjab^}4HoY#%hLd1OFg&*tl8A0 zvq#xtH7Tj$*@IL$KC?Ncg|q>1{Io&#pa*$|kTzZ=w)vfm;BG|n28tXbO%({*6wz1j zS5@ic{OXUfOH9%$%}|BD0S(>XFo{W4I*V}?e>9mARj39b=Y^z>vrhJDAEBB^|xMc5~2^U^5-#nt1f!38;XG}W@aWGxCc!%+B@&kv7 zhdxnQu)|ukVY8o&ap&GAd|3eRJniZ{==2b}JqXk(U_IU39x^gt5+00J{9+kuXxRFb zhl>e{7ck7~aMJLW@XCK}kOy<4V-qdi65>YTxVo@l{R#IyoizDM~unEzuwFv6>7E=hKZ!5Mt>C8PqJ zA-?G%>O3l+E+Vz{7gSMfO01EhUUhLb98Anpms!MAVJzzJqY0nZP)m598mFD0^I2@w zPWFDKF#%mvs#IRyDC)kUs$J6s_8XqeyuQPi+Tb}2KUP2d$Uo%!$fI@@pVri0EO>A- z#T1)9&nXPKf$032vfE33%k^kOY-0xZE&X0>hZ@gL{KsaCeOTwez`J|XP783a4tcx( zx|!YHz2Eo%m#s+7!oHS(dTDXI?8HGWt$Gqsu51TBGpq<4)d!dTdVRV|DT~jq zgELQ4cy1~6I0yf=>ADs4ewe{pr))|sX*lQZhz}njC31~kLY$@*J8zR`D%OHWv?5PL z&~Hk0RSkURdlb>!DDEYh$1kf6`38+fhq?Di{aqg~X!*Q5LK(VtqMCHhwfHSUChchy z)`D&J0*Yu+O53IbgH|39f^LMARk0lbJ=K<;Lb%7jxgV{KyZR;Z&W6 zdVUg|5&AjU5;x#^GI`am$upUUU9V z_kxGH7wlA`%8^<9()Zjh{6B?I<%F@obNemzcs9*4mrlsK!?Gl)@#4Gr{$4CkvZaxbne7D zHr){0Q2+9v+=PR^Ftj-*n4Dfk$yRA_*G;`#_2*dGI|u7)G@%l9j^|pdS648>xt;WXj0CJ<;?!aV&-zp$#nSMR=T;&16Br;?G{oOi>i?7 zQNT$*OxP$lqlX3TKvp_}bW~!z*UE{*=@c22!Z2LGS;&cqNb)7@=#-aSoj05(Voep4 zr|Llnq(}<-Djnp3$}gewu$_6BQ#IsozQ`X9Td)VvRsvrvm@uxEk%2X5k5ec~)$^*+^bsCaTd1_Kqd zW)EpHG#^nKs-670JemTy z=iqB8Rl7$k46&k4_5_J+fOMONewuy~QpcL(>C@P7#)iS5vP)KJkJz!|Rv$T;b$DxN zvlr6|nkCXAA^=0<3{=Wsf>LbFTNrA;S>p7AVi)t+j>_gjY_Y2&2R6>PaOb2m8lD2s z$*pnsiI8}2J^kKmq+5f?$5bkbHcV>{Db9j8cl1#-Ptm%k4bn&%3bhfGYZ9_w@oCi9Eeb>dQbi`)muKyT;8qO9Ko+sP#_3OMH-<|Gu|7xmiNW#OdahXFcVvv4b z6%f@cA%50jyemm~2gIyi2RBRml2)4H!FF}s)73u9Dc8(k|D#P^Aj=GgUyWVzZk=$w zw+Y&X=YFH~lY)}{koGVMomcA+st9^jU`!<n?xxwn7B)^6zGO)jrN5Aw8RwLkjJ&Y zMiS0N{D7^w<$2paqlUXfFzZ}NF2q-j<@!4Vzo=!p>FQHxWDpxF{Tq4D*$I#4>w^So z=2LcX09rh#A<1njgrH!vhct)RqDdaNk;_q+^wMtNicZg#)r&|s1nXY>r7-*0PP4PX)mxFF3dky(%r(J8akN7s#;Kd{k!Cf&=lOjZ4B>VSf3!m$ zuj4j+2P@sk_XF}@%Z7DXE{xbu+31G`0Qhkh0t#l3+#2Y)R3o^Za zJkaO#w(w1IBZltT*ITIwQBfBTz0$H}N}=c5y%8^cB3W5A9;l86mb`wX(JE#gS@UyC;R9{4eTkP`BSihK7vQmy=Xq>zNRz%S7Yk#3?qRBL(50m+Y0)_}STyXkn zF&+Tz^z%29-=WnfB|Pup9rpLvsbH283AiBT0@5;bNlO{i#!(0Xja5MTM4n;nv|^5G zfVnhc0l{SYR~hfXVVPk2S4DdYrOl>**krHK%dC-IKGP;w@((Lv7{1;+ji%R(a;tqL zR=O0VX!{wbvKcSi?FJ~ZasG!&uee`Ku62mnL2+lHg1W>WS<8l7DJv zj=D?|=*lAY?#nWIRvz*gs(3lJAHXH=W;O^1HBHZOi@hI_n8B5L=cm44W7pGe6VOd6twW#yBvs;c8Z$Z4j9{(q^ z*vU;}?fnI5$KkR5Cev3^vn;u0yP@hxMVt`bQVpbD0R=BAOdrTGClwrX)6?A$_ zY`a_ULt5#(K*M|gbDL9ZKUX@+(XQ&ECl%%DDTuEbE*i{oS?}Tg-zK0@b21B_pBMK2 z=Y{>OQ%8n9` znICv{3_wg0;C;bghF*nAj7f<~z+R@@>r-fZ47|YCl4*e6imZ7^mKb@=C@jFJ#mU+1 zLHUw;wcAya+yf>MjArwbTf^c|0+_QK3-jh^Rfy987@*clHoXjn81rCY{iI>exRXI$ zf3)G0FtwSgX^yngvn!{5z%53a=C(AUtx}mWx+{}!NCFDUNTn9j;nLZL$oNOrRa9OGots!reP1C!6h;)Qp zHw7iu^vOqLnI=>Y;Hj`R2*>kExc;(aTfGRP3-I`0A# zcAkc?6Zfi;(K0Nt%U@Zzf+?QyO_S!8Tnj6Szw|O34_LbP6=L2)7!k`EoPSHk8po|K z>-2%hH0*5{XwcMsg5yQD8Ti^OdK9O#h&(|L8@;n^`>%bLcQ_g}>IcgaWd;CH{_pkK|A4U# zZLO?~4V{#2oE?pgRP-Ip{{i{^6Vx8U{$WFL-RXX0lmqyKK*%PDV1&NBV36s1H$U|e z#MIpe>tzCH{;;7o+Z&7KR^k?Oq%Rei49ovMS5Eg}KM!;qx-r;xZqs3Hk1Xi#d+%E? zWW)VY$vr+lyjf4}+jP5e=G5xWp~eY_46GPfUaA*sk(Bm<3=@bfui&9(V&*F z_bI$Fu83$xxHO;^z89QBE{KdRaGXcVz8-?*Z=JiTDs4Z3u8R`}|4Jd#&R1z?N8js| z1NbhYTMRXTi8WO2#nWyKGRuAl@{nuntvZX~(-@Mb7{vC%Yq6a$AV8$-Jk?a+=$(M= zE-R;Y0i3QG)~#{A1@^%lE1(cc2p(HxM1^w~3nbGLsaO6*KRY4wi&#Ox1GMRKpc059 zYG(k#SzcP~GMJ-0c-%7AX?+FQT9`OBOvbXJbB~D03^H2`{e?f^kWxuZTL4NH1`66_Sc3m zcA1}|r_S2h1INd&EssPvcS*j6x%7}DIp%6iNtu6GdVQTn@h2|b^B55!QAfYV#n?mFc%3+ zvD7&j6R`O%$}KSg^ld+kqo`1EK0qqGzH8o;hbJY@3MNT^9z#YMkId0MC|J&Tg4(M- zYJ|byL_l)FSr`d6A^fbc7&cDW&3F;v^u8dEem#eTm_AQRIRrYHV^A@M7c19k9rI~| z%NxEz7w$Tjq!Kag`Nj2;>Z*!skQ4FUix@Bh(!JWPjZL zD6SQH5t2K)pN3^m8;t0@8I}mQcC#N zIaUi4yaoYVfE1}v^}TSFMHrM3`?$q4N67SF9t4A)4nTx&lW+^j5s}oQ@$=AvWef4$ zs!`z&_@)=k_!rC6H9Ec}1d#*3EIU!Ym!2x1%Csco;?4F8VqZTQP@gM~Q2ba_0ksc? zGf*Pcw=RfYG7ImPpazB=1Q)fya?+m|^Ai=rooyzvtcQn5O(-oM zbi?;$KrOu)#a;XoNVHuCLr)wCt<^x!T+s$;Q;)O~Yh5MJMO`G{fpln{5tO^9Eim2- z5YTJ$hi_;YVEl8#&T~9R2zXcR2DKW)!*>a5PnZwmrqJcc08U1eVKvhe_91befOC9j zC$c4yqfU2UhGiK9tt|GY5x`)6dFtoPCThDGIh}p3A)dE0!f6@d#n|5fg8`Xqk2p zNnxZ@>{K7&04w-T7{ZqNIx*%G{cs03$U3VGf)`tC+UkdR;^i&st~Lbu^EhlrttbN% zhm6{Q@f5IF9sCzt-7CEjigpM=_|FW`c2+n?wG#cRlBr3A<;YwqV^faT1*^8-1!TBm zxYM3aDd}bK4U0nurBg@N)e}9m5a$GX!YPx?{3L=K7)WtGBdo7iUf}(&^8`N?9xEq# zI#eX{V6I0O?8~a%Ix;fW>(Qhc?73=G^I%a;u{bz-^H#xmw?My zM(R~xf*?7t^obIGYa2r(6CO25r5Ny~zj^{vV}~aw{hJil5ROU25e@g#L¬j?9Oj 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�MA*YpU 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 z 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 %2=g$lB6_o>|jJGU=O~b4eCP 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%0AiZ)%!CVN~9BatlLc^r?6^ylwo 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 @>=X6;0^8b>dII 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