diff --git a/protos/SignalStorage.proto b/protos/SignalStorage.proto index e2ee1b4c27..1455a86f5f 100644 --- a/protos/SignalStorage.proto +++ b/protos/SignalStorage.proto @@ -1,10 +1,15 @@ -// Copyright 2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only +/* + * Copyright 2020-2021 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +syntax = "proto3"; package signalservice; -option java_package = "org.whispersystems.signalservice.internal.storage"; -option java_outer_classname = "SignalStorageProtos"; +option java_package = "org.signal.storageservice.storage.protos.contacts"; +option java_outer_classname = "StorageProtos"; +option java_multiple_files = true; enum OptionalBool { UNSET = 0; @@ -12,29 +17,29 @@ enum OptionalBool { DISABLED = 2; } -message StorageManifest { - optional uint64 version = 1; - optional bytes value = 2; +message StorageManifest { + uint64 version = 1; + bytes value = 2; } message StorageItem { - optional bytes key = 1; - optional bytes value = 2; + bytes key = 1; + bytes value = 2; } message StorageItems { repeated StorageItem items = 1; } -message ReadOperation { - repeated bytes readKey = 1; +message WriteOperation { + StorageManifest manifest = 1; + repeated StorageItem insertItem = 2; + repeated bytes deleteKey = 3; + bool clearAll = 4; } -message WriteOperation { - optional StorageManifest manifest = 1; - repeated StorageItem insertItem = 2; - repeated bytes deleteKey = 3; - optional bool clearAll = 4; +message ReadOperation { + repeated bytes readKey = 1; } message ManifestRecord { @@ -48,16 +53,17 @@ message ManifestRecord { STORY_DISTRIBUTION_LIST = 5; STICKER_PACK = 6; CALL_LINK = 7; + CHAT_FOLDER = 8; } - optional bytes raw = 1; - optional Type type = 2; + bytes raw = 1; + Type type = 2; } - optional uint64 version = 1; - optional uint32 sourceDevice = 3; - repeated Identifier keys = 2; - optional bytes recordIkm = 4; + uint64 version = 1; + uint32 sourceDevice = 3; + repeated Identifier identifiers = 2; + bytes recordIkm = 4; // Next ID: 5 } @@ -70,6 +76,7 @@ message StorageRecord { StoryDistributionListRecord storyDistributionList = 5; StickerPackRecord stickerPack = 6; CallLinkRecord callLink = 7; + ChatFolderRecord chatFolder = 8; } } @@ -105,43 +112,44 @@ message ContactRecord { } message Name { - optional string given = 1; - optional string family = 2; + string given = 1; + string family = 2; } - optional string aci = 1; - optional string serviceE164 = 2; - optional string pni = 15; - optional bytes profileKey = 3; - optional bytes identityKey = 4; - optional IdentityState identityState = 5; - optional string givenName = 6; - optional string familyName = 7; - optional string username = 8; - optional bool blocked = 9; - optional bool whitelisted = 10; - optional bool archived = 11; - optional bool markedUnread = 12; - optional uint64 mutedUntilTimestamp = 13; - optional bool hideStory = 14; - optional uint64 unregisteredAtTimestamp = 16; - optional string systemGivenName = 17; - optional string systemFamilyName = 18; - optional string systemNickname = 19; - optional bool hidden = 20; - optional bool pniSignatureVerified = 21; - optional Name nickname = 22; - optional string note = 23; + string aci = 1; + string e164 = 2; + string pni = 15; + bytes profileKey = 3; + bytes identityKey = 4; + IdentityState identityState = 5; + string givenName = 6; + string familyName = 7; + string username = 8; + bool blocked = 9; + bool whitelisted = 10; + bool archived = 11; + bool markedUnread = 12; + uint64 mutedUntilTimestamp = 13; + bool hideStory = 14; + uint64 unregisteredAtTimestamp = 16; + string systemGivenName = 17; + string systemFamilyName = 18; + string systemNickname = 19; + bool hidden = 20; + bool pniSignatureVerified = 21; + Name nickname = 22; + string note = 23; optional AvatarColor avatarColor = 24; + // Next ID: 25 } message GroupV1Record { - optional bytes id = 1; - optional bool blocked = 2; - optional bool whitelisted = 3; - optional bool archived = 4; - optional bool markedUnread = 5; - optional uint64 mutedUntilTimestamp = 6; + bytes id = 1; + reserved /*blocked*/ 2; + reserved /*whitelisted*/ 3; + reserved /*archived*/ 4; + reserved /*markedUnread*/ 5; + reserved /*mutedUntilTimestamp*/ 6; } message GroupV2Record { @@ -151,36 +159,37 @@ message GroupV2Record { ENABLED = 2; } - optional bytes masterKey = 1; - optional bool blocked = 2; - optional bool whitelisted = 3; - optional bool archived = 4; - optional bool markedUnread = 5; - optional uint64 mutedUntilTimestamp = 6; - optional bool dontNotifyForMentionsIfMuted = 7; - optional bool hideStory = 8; - reserved /* storySendEnabled */ 9; // removed - optional StorySendMode storySendMode = 10; + bytes masterKey = 1; + bool blocked = 2; + bool whitelisted = 3; + bool archived = 4; + bool markedUnread = 5; + uint64 mutedUntilTimestamp = 6; + bool dontNotifyForMentionsIfMuted = 7; + bool hideStory = 8; + reserved 9; + StorySendMode storySendMode = 10; optional AvatarColor avatarColor = 11; } message AccountRecord { + enum PhoneNumberSharingMode { - UNKNOWN = 0; + UNKNOWN = 0; EVERYBODY = 1; - NOBODY = 2; + NOBODY = 2; } message PinnedConversation { message Contact { - optional string serviceId = 1; - optional string e164 = 2; + string serviceId = 1; + string e164 = 2; } oneof identifier { - Contact contact = 1; - bytes legacyGroupId = 3; - bytes groupMasterKey = 4; + Contact contact = 1; + bytes legacyGroupId = 3; + bytes groupMasterKey = 4; } } @@ -197,13 +206,13 @@ message AccountRecord { PURPLE = 8; } - optional bytes entropy = 1; // 32 bytes of entropy used for encryption - optional bytes serverId = 2; // 16 bytes of encoded UUID provided by the server - optional Color color = 3; // color of the QR code itself + bytes entropy = 1; // 32 bytes of entropy used for encryption + bytes serverId = 2; // 16 bytes of encoded UUID provided by the server + Color color = 3; // color of the QR code itself } message IAPSubscriberData { - optional bytes subscriberId = 1; + bytes subscriberId = 1; oneof iapSubscriptionId { // Identifies an Android Play Store IAP subscription. @@ -213,84 +222,128 @@ message AccountRecord { } } - optional bytes profileKey = 1; - optional string givenName = 2; - optional string familyName = 3; - optional string avatarUrl = 4; - optional bool noteToSelfArchived = 5; - optional bool readReceipts = 6; - optional bool sealedSenderIndicators = 7; - optional bool typingIndicators = 8; + message BackupTierHistory { + // See zkgroup for integer particular values. Unset if backups are not enabled. + optional uint64 backupTier = 1; + optional uint64 endedAtTimestamp = 2; + } + + bytes profileKey = 1; + string givenName = 2; + string familyName = 3; + string avatarUrlPath = 4; + bool noteToSelfArchived = 5; + bool readReceipts = 6; + bool sealedSenderIndicators = 7; + bool typingIndicators = 8; reserved 9; // proxiedLinkPreviews - optional bool noteToSelfMarkedUnread = 10; - optional bool linkPreviews = 11; - optional PhoneNumberSharingMode phoneNumberSharingMode = 12; - optional bool notDiscoverableByPhoneNumber = 13; + bool noteToSelfMarkedUnread = 10; + bool linkPreviews = 11; + PhoneNumberSharingMode phoneNumberSharingMode = 12; + bool unlistedPhoneNumber = 13; repeated PinnedConversation pinnedConversations = 14; - optional bool preferContactAvatars = 15; - optional uint32 universalExpireTimer = 17; + bool preferContactAvatars = 15; + uint32 universalExpireTimer = 17; reserved 18; // primarySendsSms reserved 19; // deprecatedE164 repeated string preferredReactionEmoji = 20; - optional bytes subscriberId = 21; - optional string subscriberCurrencyCode = 22; - optional bool displayBadgesOnProfile = 23; - optional bool donorSubscriptionManuallyCancelled = 24; - optional bool keepMutedChatsArchived = 25; - optional bool hasSetMyStoriesPrivacy = 26; - optional bool hasViewedOnboardingStory = 27; + bytes donorSubscriberId = 21; + string donorSubscriberCurrencyCode = 22; + bool displayBadgesOnProfile = 23; + bool donorSubscriptionManuallyCancelled = 24; + bool keepMutedChatsArchived = 25; + bool hasSetMyStoriesPrivacy = 26; + bool hasViewedOnboardingStory = 27; // Whether the user has opened and played back the + // onboarding story in the story viewer. reserved 28; // deprecatedStoriesDisabled - optional bool storiesDisabled = 29; - optional OptionalBool storyViewReceiptsEnabled = 30; + bool storiesDisabled = 29; + OptionalBool storyViewReceiptsEnabled = 30; reserved 31; // hasReadOnboardingStory - optional bool hasSeenGroupStoryEducationSheet = 32; - optional string username = 33; - optional bool hasCompletedUsernameOnboarding = 34; - optional UsernameLink usernameLink = 35; + bool hasSeenGroupStoryEducationSheet = 32; // Whether the user has seen the group story education + // sheet. This is a sticky value. + string username = 33; // Format: `nickname.discriminator`, e.g. `signalapp.123` + // Updated only when username is confirmed or deleted on server. + bool hasCompletedUsernameOnboarding = 34; // Whether the user has completed username + // onboarding. + UsernameLink usernameLink = 35; reserved /*backupsSubscriberId*/ 36; reserved /*backupsSubscriberCurrencyCode*/ 37; reserved /*backupsSubscriptionManuallyCancelled*/ 38; // Set to true after backups are enabled and one is uploaded. optional bool hasBackup = 39; - // See zkgroup for integer particular values + // See zkgroup for integer particular values. Unset if backups are not enabled. optional uint64 backupTier = 40; - optional IAPSubscriberData backupSubscriberData = 41; + IAPSubscriberData backupSubscriberData = 41; optional AvatarColor avatarColor = 42; } message StoryDistributionListRecord { - optional bytes identifier = 1; - optional string name = 2; + bytes identifier = 1; + string name = 2; repeated string recipientServiceIds = 3; - optional uint64 deletedAtTimestamp = 4; - optional bool allowsReplies = 5; - optional bool isBlockList = 6; + uint64 deletedAtTimestamp = 4; + bool allowsReplies = 5; + bool isBlockList = 6; } message StickerPackRecord { - optional bytes packId = 1; // 16 bytes - optional bytes packKey = 2; // 32 bytes, used to derive the AES-256 key - // aesKey = HKDF( - // input = packKey, - // salt = 32 zero bytes, - // info = "Sticker Pack" - // ) - optional uint32 position = 3; // When displayed sticker packs should be first sorted - // in descending order by zero-based `position` and - // then by ascending `packId` (lexicographically, - // packId can be treated as a hex string). - // When installing a sticker pack the client should find - // the maximum `position` among currently known stickers - // and use `max_position + 1` as the value for the new - // `position`. - optional uint64 deletedAtTimestamp = 4; // Timestamp in milliseconds. When present and - // non-zero - `packKey` and `position` should - // be unset + bytes packId = 1; // 16 bytes + bytes packKey = 2; // 32 bytes, used to derive the AES-256 key + // aesKey = HKDF( + // input = packKey, + // salt = 32 zero bytes, + // info = "Sticker Pack" + // ) + uint32 position = 3; // When displayed sticker packs should be first sorted + // in descending order by zero-based `position` and + // then by ascending `packId` (lexicographically, + // packId can be treated as a hex string). + // When installing a sticker pack the client should find + // the maximum `position` among currently known stickers + // and use `max_position + 1` as the value for the new + // `position`. + uint64 deletedAtTimestamp = 4; // Timestamp in milliseconds. When present and + // non-zero - `packKey` and `position` should + // be unset } message CallLinkRecord { - optional bytes rootKey = 1; // 16 bytes - optional bytes adminPasskey = 2; // Non-empty when the current user is an admin - optional uint64 deletedAtTimestampMs = 3; // When present and non-zero, `adminPasskey` - // should be cleared + bytes rootKey = 1; // 16 bytes + bytes adminPasskey = 2; // Non-empty when the current user is an admin + uint64 deletedAtTimestampMs = 3; // When present and non-zero, `adminPasskey` + // should be cleared +} + +message ChatFolderRecord { + message Recipient { + message Contact { + string serviceId = 1; + string e164 = 2; + } + + oneof identifier { + Contact contact = 1; + bytes legacyGroupId = 2; + bytes groupMasterKey = 3; + } + } + + // Represents the default "All chats" folder record vs all other custom folders + enum FolderType { + UNKNOWN = 0; + ALL = 1; + CUSTOM = 2; + } + + bytes id = 1; + string name = 2; + uint32 position = 3; // Position order of folder, low-to-high from start-to-end + bool showOnlyUnread = 4; + bool showMutedChats = 5; + bool includeAllIndividualChats = 6; // Folder includes all 1:1 chats, unless excluded + bool includeAllGroupChats = 7; // Folder includes all group chats, unless excluded + FolderType folderType = 8; + repeated Recipient includedRecipients = 9; + repeated Recipient excludedRecipients = 10; + uint64 deletedAtTimestampMs = 11; // When non-zero, `position` should be set to -1 and `includedRecipients` should be empty } diff --git a/ts/services/storage.ts b/ts/services/storage.ts index ac65fb8545..26484c6215 100644 --- a/ts/services/storage.ts +++ b/ts/services/storage.ts @@ -741,7 +741,7 @@ async function generateManifest( const pendingDeletes: Set = new Set(); const remoteKeys: Set = new Set(); - (previousManifest.keys ?? []).forEach( + (previousManifest.identifiers ?? []).forEach( (identifier: IManifestRecordIdentifier) => { strictAssert(identifier.raw, 'Identifier without raw field'); const storageID = Bytes.toBase64(identifier.raw); @@ -874,7 +874,7 @@ async function encryptManifest( const manifestRecord = new Proto.ManifestRecord(); manifestRecord.version = Long.fromNumber(version); manifestRecord.sourceDevice = window.storage.user.getDeviceId() ?? 0; - manifestRecord.keys = Array.from(manifestRecordKeys); + manifestRecord.identifiers = Array.from(manifestRecordKeys); if (recordIkm != null) { manifestRecord.recordIkm = recordIkm; } @@ -1292,10 +1292,12 @@ async function processManifest( } const remoteKeysTypeMap = new Map(); - (manifest.keys || []).forEach(({ raw, type }: IManifestRecordIdentifier) => { - strictAssert(raw, 'Identifier without raw field'); - remoteKeysTypeMap.set(Bytes.toBase64(raw), type); - }); + (manifest.identifiers || []).forEach( + ({ raw, type }: IManifestRecordIdentifier) => { + strictAssert(raw, 'Identifier without raw field'); + remoteKeysTypeMap.set(Bytes.toBase64(raw), type); + } + ); const remoteKeys = new Set(remoteKeysTypeMap.keys()); const localVersions = new Map(); @@ -1822,7 +1824,7 @@ async function processRemoteRecords( }); // Find remote contact records that: - // - Have `remote.pni` and have `remote.serviceE164` + // - Have `remote.pni` and have `remote.e164` // - Match local contact that has `aci`. const splitPNIContacts = new Array(); prunedStorageItems = prunedStorageItems.filter(item => { @@ -1832,7 +1834,7 @@ async function processRemoteRecords( return true; } - if (!contact.serviceE164 || !contact.pni) { + if (!contact.e164 || !contact.pni) { return true; } diff --git a/ts/services/storageRecordOps.ts b/ts/services/storageRecordOps.ts index da521662c6..7504818830 100644 --- a/ts/services/storageRecordOps.ts +++ b/ts/services/storageRecordOps.ts @@ -230,7 +230,7 @@ export async function toContactRecord( } const e164 = conversation.get('e164'); if (e164) { - contactRecord.serviceE164 = e164; + contactRecord.e164 = e164; } const username = conversation.get('username'); const ourID = window.ConversationController.getOurConversationId(); @@ -334,7 +334,7 @@ export function toAccountRecord( } const avatarUrl = window.storage.get('avatarUrl'); if (avatarUrl !== undefined) { - accountRecord.avatarUrl = avatarUrl; + accountRecord.avatarUrlPath = avatarUrl; } const username = conversation.get('username'); if (username !== undefined) { @@ -394,10 +394,10 @@ export function toAccountRecord( ); switch (phoneNumberDiscoverability) { case PhoneNumberDiscoverability.Discoverable: - accountRecord.notDiscoverableByPhoneNumber = false; + accountRecord.unlistedPhoneNumber = false; break; case PhoneNumberDiscoverability.NotDiscoverable: - accountRecord.notDiscoverableByPhoneNumber = true; + accountRecord.unlistedPhoneNumber = true; break; default: throw missingCaseError(phoneNumberDiscoverability); @@ -454,11 +454,11 @@ export function toAccountRecord( const subscriberId = window.storage.get('subscriberId'); if (Bytes.isNotEmpty(subscriberId)) { - accountRecord.subscriberId = subscriberId; + accountRecord.donorSubscriberId = subscriberId; } const subscriberCurrencyCode = window.storage.get('subscriberCurrencyCode'); if (typeof subscriberCurrencyCode === 'string') { - accountRecord.subscriberCurrencyCode = subscriberCurrencyCode; + accountRecord.donorSubscriberCurrencyCode = subscriberCurrencyCode; } const donorSubscriptionManuallyCancelled = window.storage.get( 'donorSubscriptionManuallyCancelled' @@ -555,14 +555,6 @@ export function toGroupV1Record( const groupV1Record = new Proto.GroupV1Record(); groupV1Record.id = Bytes.fromBinary(String(conversation.get('groupId'))); - groupV1Record.blocked = conversation.isBlocked(); - groupV1Record.whitelisted = Boolean(conversation.get('profileSharing')); - groupV1Record.archived = Boolean(conversation.get('isArchived')); - groupV1Record.markedUnread = Boolean(conversation.get('markedUnread')); - groupV1Record.mutedUntilTimestamp = getSafeLongFromTimestamp( - conversation.get('muteExpiresAt'), - Long.MAX_VALUE - ); applyUnknownFields(groupV1Record, conversation); @@ -720,7 +712,7 @@ export function toDefunctOrPendingCallLinkRecord( return callLinkRecord; } -type MessageRequestCapableRecord = Proto.IContactRecord | Proto.IGroupV1Record; +type MessageRequestCapableRecord = Proto.IContactRecord | Proto.IGroupV2Record; function applyMessageRequestState( record: MessageRequestCapableRecord, @@ -940,24 +932,10 @@ export async function mergeGroupV1Record( } conversation.set({ - isArchived: Boolean(groupV1Record.archived), - markedUnread: Boolean(groupV1Record.markedUnread), storageID, storageVersion, }); - conversation.setMuteExpiration( - getTimestampFromLong( - groupV1Record.mutedUntilTimestamp, - Number.MAX_SAFE_INTEGER - ), - { - viaStorageServiceSync: true, - } - ); - - applyMessageRequestState(groupV1Record, conversation); - let hasPendingChanges: boolean; if (isGroupV1(conversation.attributes)) { @@ -1171,7 +1149,7 @@ export async function mergeContactRecord( : undefined, }; - const e164 = dropNull(contactRecord.serviceE164); + const e164 = dropNull(contactRecord.e164); const { aci } = contactRecord; const pni = dropNull(contactRecord.pni); const pniSignatureVerified = contactRecord.pniSignatureVerified || false; @@ -1386,7 +1364,7 @@ export async function mergeAccountRecord( let details = new Array(); const { linkPreviews, - notDiscoverableByPhoneNumber, + unlistedPhoneNumber, noteToSelfArchived, noteToSelfMarkedUnread, phoneNumberSharingMode, @@ -1398,8 +1376,8 @@ export async function mergeAccountRecord( preferContactAvatars, universalExpireTimer, preferredReactionEmoji: rawPreferredReactionEmoji, - subscriberId, - subscriberCurrencyCode, + donorSubscriberId, + donorSubscriberCurrencyCode, donorSubscriptionManuallyCancelled, backupSubscriberData, backupTier, @@ -1486,7 +1464,7 @@ export async function mergeAccountRecord( phoneNumberSharingModeToStore ); - const discoverability = notDiscoverableByPhoneNumber + const discoverability = unlistedPhoneNumber ? PhoneNumberDiscoverability.NotDiscoverable : PhoneNumberDiscoverability.Discoverable; await window.storage.put('phoneNumberDiscoverability', discoverability); @@ -1605,11 +1583,14 @@ export async function mergeAccountRecord( ); } - if (Bytes.isNotEmpty(subscriberId)) { - await window.storage.put('subscriberId', subscriberId); + if (Bytes.isNotEmpty(donorSubscriberId)) { + await window.storage.put('subscriberId', donorSubscriberId); } - if (typeof subscriberCurrencyCode === 'string') { - await window.storage.put('subscriberCurrencyCode', subscriberCurrencyCode); + if (typeof donorSubscriberCurrencyCode === 'string') { + await window.storage.put( + 'subscriberCurrencyCode', + donorSubscriberCurrencyCode + ); } if (donorSubscriptionManuallyCancelled != null) { await window.storage.put( @@ -1748,7 +1729,7 @@ export async function mergeAccountRecord( { viaStorageServiceSync: true, reason: 'mergeAccountRecord' } ); - const avatarUrl = dropNull(accountRecord.avatarUrl); + const avatarUrl = dropNull(accountRecord.avatarUrlPath); await conversation.setAndMaybeFetchProfileAvatar({ avatarUrl, decryptionKey: profileKey,