diff --git a/protos/Backups.proto b/protos/Backups.proto
index 40900a3de2f..cdb0b309356 100644
--- a/protos/Backups.proto
+++ b/protos/Backups.proto
@@ -9,8 +9,7 @@ option java_package = "org.thoughtcrime.securesms.backup.v2.proto";
 message BackupInfo {
   uint64 version = 1;
   uint64 backupTimeMs = 2;
-  bytes mediaRootBackupKey = 3; // 32-byte random value generated when the
-                                // backup is uploaded for the first time.
+  bytes mediaRootBackupKey = 3; // 32-byte random value generated when the backup is uploaded for the first time.
 }
 
 // Frames must follow in the following ordering rules:
@@ -112,6 +111,12 @@ message Recipient {
 }
 
 message Contact {
+  enum IdentityState {
+    DEFAULT = 0;
+    VERIFIED = 1;
+    UNVERIFIED = 2;
+  }
+
   message Registered { }
   message NotRegistered {
     uint64 unregisteredTimestamp = 1;
@@ -140,6 +145,8 @@ message Contact {
   optional string profileGivenName = 11;
   optional string profileFamilyName = 12;
   bool hideStory = 13;
+  optional bytes identityKey = 14;
+  IdentityState identityState = 15;
 }
 
 message Group {
@@ -240,9 +247,9 @@ message Chat {
   uint64 id = 1; // generated id for reference only within this file
   uint64 recipientId = 2;
   bool archived = 3;
-  uint32 pinnedOrder = 4; // 0 = unpinned, otherwise chat is considered pinned and will be displayed in ascending order
-  uint64 expirationTimerMs = 5; // 0 = no expire timer.
-  uint64 muteUntilMs = 6;
+  optional uint32 pinnedOrder = 4; // will be displayed in ascending order
+  optional uint64 expirationTimerMs = 5;
+  optional uint64 muteUntilMs = 6; // UINT64_MAX (2^63 - 1) = "always muted".
   bool markedUnread = 7;
   bool dontNotifyForMentionsIfMuted = 8;
   ChatStyle style = 9;
@@ -268,7 +275,7 @@ message CallLink {
   optional bytes adminKey = 2; // Only present if the user is an admin
   string name = 3;
   Restrictions restrictions = 4;
-  uint64 expirationMs = 5;
+  optional uint64 expirationMs = 5;
 }
 
 message AdHocCall {
@@ -285,6 +292,8 @@ message AdHocCall {
 }
 
 message DistributionListItem {
+  // distribution ids are UUIDv4s. "My Story" is represented
+  // by an all-0 UUID (00000000-0000-0000-0000-000000000000).
   bytes distributionId = 1; // distribution list ids are uuids
 
   oneof item {
@@ -310,7 +319,7 @@ message DistributionList {
 message ChatItem {
   message IncomingMessageDetails {
     uint64 dateReceived = 1;
-    uint64 dateServerSent = 2;
+    optional uint64 dateServerSent = 2;
     bool read = 3;
     bool sealedSender = 4;
   }
@@ -325,8 +334,8 @@ message ChatItem {
   uint64 chatId = 1;   // conversation id
   uint64 authorId = 2; // recipient id
   uint64 dateSent = 3;
-  uint64 expireStartDate = 4; // timestamp of when expiration timer started ticking down
-  uint64 expiresInMs = 5; // how long timer of message is (ms)
+  optional uint64 expireStartDate = 4; // timestamp of when expiration timer started ticking down
+  optional uint64 expiresInMs = 5; // how long timer of message is (ms)
   repeated ChatItem revisions = 6; // ordered from oldest to newest
   bool sms = 7;
 
@@ -616,7 +625,7 @@ message FilePointer {
   message AttachmentLocator {
     string cdnKey = 1;
     uint32 cdnNumber = 2;
-    uint64 uploadTimestamp = 3;
+    optional uint64 uploadTimestamp = 3;
     bytes key = 4;
     bytes digest = 5;
     uint32 size = 6;
@@ -650,7 +659,8 @@ message Quote {
   enum Type {
     UNKNOWN = 0;
     NORMAL = 1;
-    GIFTBADGE = 2;
+    GIFT_BADGE = 2;
+    VIEW_ONCE = 3;
   }
 
   message QuotedAttachment {
@@ -768,8 +778,7 @@ message GroupCall {
   optional uint64 ringerRecipientId = 3;
   optional uint64 startedCallRecipientId = 4;
   uint64 startedCallTimestamp = 5;
-  // The time the call ended. 0 indicates an unknown time.
-  uint64 endedCallTimestamp = 6;
+  optional uint64 endedCallTimestamp = 6; // The time the call ended. 
   bool read = 7;
 }
 
@@ -825,7 +834,6 @@ message SessionSwitchoverChatUpdate {
 
 message GroupChangeChatUpdate {
   message Update {
-    // Note: group expiration timer changes are represented as ExpirationTimerChatUpdate.
     oneof update {
       GenericGroupUpdate genericGroupUpdate = 1;
       GroupCreationUpdate groupCreationUpdate = 2;
diff --git a/ts/services/backups/export.ts b/ts/services/backups/export.ts
index 77a2dcc75c1..4ded3ec1859 100644
--- a/ts/services/backups/export.ts
+++ b/ts/services/backups/export.ts
@@ -17,7 +17,10 @@ import {
   pauseWriteAccess,
   resumeWriteAccess,
 } from '../../sql/Client';
-import type { PageMessagesCursorType } from '../../sql/Interface';
+import type {
+  PageMessagesCursorType,
+  IdentityKeyType,
+} from '../../sql/Interface';
 import * as log from '../../logging/log';
 import { GiftBadgeStates } from '../../components/conversation/Message';
 import { type CustomColorType } from '../../types/Colors';
@@ -290,6 +293,13 @@ export class BackupExportStream extends Readable {
       stickerPacks: 0,
     };
 
+    const identityKeys = await DataReader.getAllIdentityKeys();
+    const identityKeysById = new Map(
+      identityKeys.map(key => {
+        return [key.id, key];
+      })
+    );
+
     for (const { attributes } of window.ConversationController.getAll()) {
       const recipientId = this.getRecipientId({
         id: attributes.id,
@@ -297,7 +307,11 @@ export class BackupExportStream extends Readable {
         e164: attributes.e164,
       });
 
-      const recipient = this.toRecipient(recipientId, attributes);
+      const recipient = this.toRecipient(
+        recipientId,
+        attributes,
+        identityKeysById
+      );
       if (recipient === undefined) {
         // Can't be backed up.
         continue;
@@ -804,7 +818,8 @@ export class BackupExportStream extends Readable {
     convo: Omit<
       ConversationAttributesType,
       'id' | 'version' | 'expireTimerVersion'
-    >
+    >,
+    identityKeysById?: ReadonlyMap<IdentityKeyType['id'], IdentityKeyType>
   ): Backups.IRecipient | undefined {
     const res: Backups.IRecipient = {
       id: recipientId,
@@ -824,6 +839,11 @@ export class BackupExportStream extends Readable {
         throw missingCaseError(convo.removalStage);
       }
 
+      let identityKey: IdentityKeyType | undefined;
+      if (identityKeysById != null && convo.serviceId != null) {
+        identityKey = identityKeysById.get(convo.serviceId);
+      }
+
       res.contact = {
         aci:
           convo.serviceId && convo.serviceId !== convo.pni
@@ -856,6 +876,10 @@ export class BackupExportStream extends Readable {
         profileGivenName: convo.profileName,
         profileFamilyName: convo.profileFamilyName,
         hideStory: convo.hideStory === true,
+        identityKey: identityKey?.publicKey || null,
+
+        // Integer values match so we can use it as is
+        identityState: identityKey?.verified ?? 0,
       };
     } else if (isGroupV2(convo) && convo.masterKey) {
       let storySendMode: Backups.Group.StorySendMode;
@@ -2092,6 +2116,15 @@ export class BackupExportStream extends Readable {
       return null;
     }
 
+    let quoteType: Backups.Quote.Type;
+    if (quote.isGiftBadge) {
+      quoteType = Backups.Quote.Type.GIFT_BADGE;
+    } else if (quote.isViewOnce) {
+      quoteType = Backups.Quote.Type.VIEW_ONCE;
+    } else {
+      quoteType = Backups.Quote.Type.NORMAL;
+    }
+
     return {
       targetSentTimestamp: Long.fromNumber(quote.id),
       authorId,
@@ -2123,9 +2156,7 @@ export class BackupExportStream extends Readable {
           }
         )
       ),
-      type: quote.isGiftBadge
-        ? Backups.Quote.Type.GIFTBADGE
-        : Backups.Quote.Type.NORMAL,
+      type: quoteType,
     };
   }
 
diff --git a/ts/services/backups/import.ts b/ts/services/backups/import.ts
index 0e751f188e1..fd12a69d73a 100644
--- a/ts/services/backups/import.ts
+++ b/ts/services/backups/import.ts
@@ -14,6 +14,7 @@ import { DataReader, DataWriter } from '../../sql/Client';
 import {
   AttachmentDownloadSource,
   type StoryDistributionWithMembersType,
+  type IdentityKeyType,
 } from '../../sql/Interface';
 import * as log from '../../logging/log';
 import { GiftBadgeStates } from '../../components/conversation/Message';
@@ -209,6 +210,7 @@ export class BackupImportStream extends Writable {
     string,
     ConversationAttributesType
   >();
+  private readonly identityKeys = new Map<ServiceIdString, IdentityKeyType>();
   private readonly saveMessageBatch = new Set<MessageAttributesType>();
   private readonly stickerPacks = new Array<StickerPackPointerType>();
   private ourConversation?: ConversationAttributesType;
@@ -489,10 +491,14 @@ export class BackupImportStream extends Writable {
     const saves = Array.from(this.conversations.values());
     this.conversations.clear();
 
+    const identityKeys = Array.from(this.identityKeys.values());
+    this.identityKeys.clear();
+
     // Queue writes at the same time to prevent races.
     await Promise.all([
       DataWriter.saveConversations(saves),
       DataWriter.updateConversations(updates),
+      DataWriter.bulkAddIdentityKeys(identityKeys),
     ]);
   }
 
@@ -817,11 +823,13 @@ export class BackupImportStream extends Writable {
         break;
     }
 
+    const serviceId = aci ?? pni;
+
     const attrs: ConversationAttributesType = {
       id: generateUuid(),
       type: 'private',
       version: 2,
-      serviceId: aci ?? pni,
+      serviceId,
       pni,
       e164,
       removalStage,
@@ -836,6 +844,17 @@ export class BackupImportStream extends Writable {
       expireTimerVersion: 1,
     };
 
+    if (serviceId != null && Bytes.isNotEmpty(contact.identityKey)) {
+      this.identityKeys.set(serviceId, {
+        id: serviceId,
+        publicKey: contact.identityKey,
+        verified: contact.identityState || 0,
+        firstUse: true,
+        timestamp: this.now,
+        nonblockingApproval: true,
+      });
+    }
+
     if (contact.notRegistered) {
       const timestamp = contact.notRegistered.unregisteredTimestamp?.toNumber();
       attrs.discoveredUnregisteredAt = timestamp || this.now;
@@ -848,7 +867,6 @@ export class BackupImportStream extends Writable {
     }
 
     if (contact.blocked) {
-      const serviceId = aci || pni;
       if (serviceId) {
         await window.storage.blocked.addBlockedServiceId(serviceId);
       }
@@ -1671,8 +1689,11 @@ export class BackupImportStream extends Writable {
     type: Backups.Quote.Type | null | undefined
   ): SignalService.DataMessage.Quote.Type {
     switch (type) {
-      case Backups.Quote.Type.GIFTBADGE:
+      case Backups.Quote.Type.GIFT_BADGE:
         return SignalService.DataMessage.Quote.Type.GIFT_BADGE;
+      case Backups.Quote.Type.VIEW_ONCE:
+        // No special treatment, we'll compute it once we find the message
+        return SignalService.DataMessage.Quote.Type.NORMAL;
       case Backups.Quote.Type.NORMAL:
       case Backups.Quote.Type.UNKNOWN:
       case null: