From b10835ffc7adcd97b3a0a8b21aefe8950f5631b1 Mon Sep 17 00:00:00 2001
From: sachaaaaa <sacha@loki.network>
Date: Thu, 5 Sep 2019 17:41:12 +1000
Subject: [PATCH] Share contact upon authorising secondary device

---
 app/sql.js                               |  30 +++--
 js/background.js                         |   9 +-
 js/modules/data.js                       |  15 ++-
 js/signal_protocol_store.js              |  16 +++
 js/views/standalone_registration_view.js |   2 +-
 libloki/api.js                           |  68 +++++++++-
 libtextsecure/message_receiver.js        | 152 +++++++++++++----------
 protos/SignalService.proto               |   2 +
 8 files changed, 215 insertions(+), 79 deletions(-)

diff --git a/app/sql.js b/app/sql.js
index f8dd6ca11..227f59729 100644
--- a/app/sql.js
+++ b/app/sql.js
@@ -101,7 +101,7 @@ module.exports = {
   updateConversation,
   removeConversation,
   getAllConversations,
-  getPubKeysWithFriendStatus,
+  getConversationsWithFriendStatus,
   getAllConversationIds,
   getAllPrivateConversations,
   getAllGroupsInvolvingId,
@@ -1566,7 +1566,9 @@ async function updateConversation(data) {
 
 async function removeConversation(id) {
   if (!Array.isArray(id)) {
-    await db.run(`DELETE FROM ${CONVERSATIONS_TABLE} WHERE id = $id;`, { $id: id });
+    await db.run(`DELETE FROM ${CONVERSATIONS_TABLE} WHERE id = $id;`, {
+      $id: id,
+    });
     return;
   }
 
@@ -1584,9 +1586,12 @@ async function removeConversation(id) {
 }
 
 async function getConversationById(id) {
-  const row = await db.get(`SELECT * FROM ${CONVERSATIONS_TABLE} WHERE id = $id;`, {
-    $id: id,
-  });
+  const row = await db.get(
+    `SELECT * FROM ${CONVERSATIONS_TABLE} WHERE id = $id;`,
+    {
+      $id: id,
+    }
+  );
 
   if (!row) {
     return null;
@@ -1596,24 +1601,29 @@ async function getConversationById(id) {
 }
 
 async function getAllConversations() {
-  const rows = await db.all(`SELECT json FROM ${CONVERSATIONS_TABLE} ORDER BY id ASC;`);
+  const rows = await db.all(
+    `SELECT json FROM ${CONVERSATIONS_TABLE} ORDER BY id ASC;`
+  );
   return map(rows, row => jsonToObject(row.json));
 }
 
-async function getPubKeysWithFriendStatus(status) {
+async function getConversationsWithFriendStatus(status) {
   const rows = await db.all(
-    `SELECT id FROM ${CONVERSATIONS_TABLE} WHERE
+    `SELECT * FROM ${CONVERSATIONS_TABLE} WHERE
       friendRequestStatus = $status
+      AND type = 'private'
     ORDER BY id ASC;`,
     {
       $status: status,
     }
   );
-  return map(rows, row => row.id);
+  return map(rows, row => jsonToObject(row.json));
 }
 
 async function getAllConversationIds() {
-  const rows = await db.all(`SELECT id FROM ${CONVERSATIONS_TABLE} ORDER BY id ASC;`);
+  const rows = await db.all(
+    `SELECT id FROM ${CONVERSATIONS_TABLE} ORDER BY id ASC;`
+  );
   return map(rows, row => row.id);
 }
 
diff --git a/js/background.js b/js/background.js
index ac15d198e..827f282a1 100644
--- a/js/background.js
+++ b/js/background.js
@@ -1113,12 +1113,19 @@
         }
       }
 
+      // Do not set name to allow working with lokiProfile and nicknames
       conversation.set({
-        name: details.name,
+        // name: details.name,
         color: details.color,
         active_at: activeAt,
       });
 
+      await conversation.setLokiProfile({ displayName: details.name });
+
+      if (details.nickname) {
+        await conversation.setNickname(details.nickname);
+      }
+
       // Update the conversation avatar only if new avatar exists and hash differs
       const { avatar } = details;
       if (avatar && avatar.data) {
diff --git a/js/modules/data.js b/js/modules/data.js
index cc78b7933..b5b1e72be 100644
--- a/js/modules/data.js
+++ b/js/modules/data.js
@@ -122,6 +122,7 @@ module.exports = {
 
   getAllConversations,
   getPubKeysWithFriendStatus,
+  getConversationsWithFriendStatus,
   getAllConversationIds,
   getAllPrivateConversations,
   getAllGroupsInvolvingId,
@@ -782,8 +783,20 @@ async function _removeConversations(ids) {
   await channels.removeConversation(ids);
 }
 
+async function getConversationsWithFriendStatus(
+  status,
+  { ConversationCollection }
+) {
+  const conversations = await channels.getConversationsWithFriendStatus(status);
+
+  const collection = new ConversationCollection();
+  collection.add(conversations);
+  return collection;
+}
+
 async function getPubKeysWithFriendStatus(status) {
-  return channels.getPubKeysWithFriendStatus(status);
+  const conversations = await getConversationsWithFriendStatus(status);
+  return conversations.map(row => row.id);
 }
 
 async function getAllConversations({ ConversationCollection }) {
diff --git a/js/signal_protocol_store.js b/js/signal_protocol_store.js
index 33d07d2f1..631433d81 100644
--- a/js/signal_protocol_store.js
+++ b/js/signal_protocol_store.js
@@ -39,6 +39,21 @@
     return false;
   }
 
+  function convertVerifiedStatusToProtoState(status) {
+    switch (status) {
+      case VerifiedStatus.VERIFIED:
+        return textsecure.protobuf.Verified.State.VERIFIED;
+
+      case VerifiedStatus.UNVERIFIED:
+        return textsecure.protobuf.Verified.State.VERIFIED;
+
+      case VerifiedStatus.DEFAULT:
+      // intentional fallthrough
+      default:
+        return textsecure.protobuf.Verified.State.DEFAULT;
+    }
+  }
+
   const StaticByteBufferProto = new dcodeIO.ByteBuffer().__proto__;
   const StaticArrayBufferProto = new ArrayBuffer().__proto__;
   const StaticUint8ArrayProto = new Uint8Array().__proto__;
@@ -913,4 +928,5 @@
   window.SignalProtocolStore = SignalProtocolStore;
   window.SignalProtocolStore.prototype.Direction = Direction;
   window.SignalProtocolStore.prototype.VerifiedStatus = VerifiedStatus;
+  window.SignalProtocolStore.prototype.convertVerifiedStatusToProtoState = convertVerifiedStatusToProtoState;
 })();
diff --git a/js/views/standalone_registration_view.js b/js/views/standalone_registration_view.js
index 8a23bb12b..3d0843cfc 100644
--- a/js/views/standalone_registration_view.js
+++ b/js/views/standalone_registration_view.js
@@ -151,7 +151,7 @@
       Whisper.Registration.remove();
       // Do not remove all items since they are only set
       // at startup.
-      textsecure.storage.remove('identityKey')
+      textsecure.storage.remove('identityKey');
       textsecure.storage.remove('secondaryDeviceStatus');
       window.ConversationController.reset();
       await window.ConversationController.load();
diff --git a/libloki/api.js b/libloki/api.js
index 66e63458b..7560c91c1 100644
--- a/libloki/api.js
+++ b/libloki/api.js
@@ -1,4 +1,4 @@
-/* global window, textsecure, log */
+/* global window, textsecure, log, Whisper, dcodeIO, StringView */
 
 // eslint-disable-next-line func-names
 (function() {
@@ -98,7 +98,66 @@
       type,
     });
   }
-
+  // Serialise as <Element0.length><Element0><Element1.length><Element1>...
+  // This is an implementation of the reciprocal of contacts_parser.js
+  function serialiseByteBuffers(buffers) {
+    const result = new dcodeIO.ByteBuffer();
+    buffers.forEach(buffer => {
+      // bytebuffer container expands and increments
+      // offset automatically
+      result.writeVarint32(buffer.limit);
+      result.append(buffer);
+    });
+    result.limit = result.offset;
+    result.reset();
+    return result;
+  }
+  async function createContactSyncProtoMessage() {
+    const conversations = await window.Signal.Data.getConversationsWithFriendStatus(
+      window.friends.friendRequestStatusEnum.friends,
+      { ConversationCollection: Whisper.ConversationCollection }
+    );
+    // Extract required contacts information out of conversations
+    const rawContacts = conversations.map(conversation => {
+      const profile = conversation.getLokiProfile();
+      const number = conversation.getNumber();
+      const name = profile
+        ? profile.displayName
+        : conversation.getProfileName();
+      const status = conversation.safeGetVerified();
+      const protoState = textsecure.storage.protocol.convertVerifiedStatusToProtoState(
+        status
+      );
+      const verified = new textsecure.protobuf.Verified({
+        state: protoState,
+        destination: number,
+        identityKey: StringView.hexToArrayBuffer(number),
+      });
+      return {
+        name,
+        verified,
+        number,
+        nickname: conversation.getNickname(),
+        blocked: conversation.isBlocked(),
+        expireTimer: conversation.get('expireTimer'),
+      };
+    });
+    // Convert raw contacts to an array of buffers
+    const contactDetails = rawContacts
+      .filter(x => x.number !== textsecure.storage.user.getNumber())
+      .map(x => new textsecure.protobuf.ContactDetails(x))
+      .map(x => x.encode());
+    // Serialise array of byteBuffers into 1 byteBuffer
+    const byteBuffer = serialiseByteBuffers(contactDetails);
+    const data = new Uint8Array(byteBuffer.toArrayBuffer());
+    const contacts = new textsecure.protobuf.SyncMessage.Contacts({
+      data,
+    });
+    const syncMessage = new textsecure.protobuf.SyncMessage({
+      contacts,
+    });
+    return syncMessage;
+  }
   async function sendPairingAuthorisation(authorisation, recipientPubKey) {
     const pairingAuthorisation = createPairingAuthorisationProtoMessage(
       authorisation
@@ -116,10 +175,14 @@
     const dataMessage = new textsecure.protobuf.DataMessage({
       profile,
     });
+    // Attach contact list
+    const syncMessage = await createContactSyncProtoMessage();
     const content = new textsecure.protobuf.Content({
       pairingAuthorisation,
       dataMessage,
+      syncMessage,
     });
+    // Send
     const options = { messageType: 'pairing-request' };
     const p = new Promise((resolve, reject) => {
       const outgoingMessage = new textsecure.OutgoingMessage(
@@ -149,5 +212,6 @@
     broadcastOnlineStatus,
     sendPairingAuthorisation,
     createPairingAuthorisationProtoMessage,
+    createContactSyncProtoMessage,
   };
 })();
diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js
index b1dffd7c8..ed7f6d191 100644
--- a/libtextsecure/message_receiver.js
+++ b/libtextsecure/message_receiver.js
@@ -1093,82 +1093,103 @@ MessageReceiver.prototype.extend({
     }
     return true;
   },
-  async handlePairingRequest(pairingRequest) {
+  async handlePairingRequest(envelope, pairingRequest) {
     const valid = await this.validateAuthorisation(pairingRequest);
-    if (!valid) {
-      return;
+    if (valid) {
+      await window.libloki.storage.savePairingAuthorisation(pairingRequest);
+      Whisper.events.trigger(
+        'devicePairingRequestReceived',
+        pairingRequest.secondaryDevicePubKey
+      );
     }
-    await window.libloki.storage.savePairingAuthorisation(pairingRequest);
-    Whisper.events.trigger(
-      'devicePairingRequestReceived',
-      pairingRequest.secondaryDevicePubKey
-    );
+    return this.removeFromCache(envelope);
   },
-  async handleAuthorisationForSelf(pairingAuthorisation, dataMessage) {
+  async handleAuthorisationForSelf(
+    envelope,
+    pairingAuthorisation,
+    { dataMessage, syncMessage }
+  ) {
     const valid = await this.validateAuthorisation(pairingAuthorisation);
-    if (!valid) {
-      return;
-    }
-    const { type, primaryDevicePubKey } = pairingAuthorisation;
-    if (type === textsecure.protobuf.PairingAuthorisationMessage.Type.GRANT) {
-      // Authorisation received to become a secondary device
-      window.log.info(
-        `Received pairing authorisation from ${primaryDevicePubKey}`
+    const alreadySecondaryDevice = !!window.storage.get('isSecondaryDevice');
+    let removedFromCache = false;
+    if (alreadySecondaryDevice) {
+      window.log.warn(
+        'Received an unexpected pairing authorisation (device is already paired as secondary device). Ignoring.'
       );
-      const alreadySecondaryDevice = !!window.storage.get('isSecondaryDevice');
-      if (alreadySecondaryDevice) {
-        window.log.warn(
-          'Received an unexpected pairing authorisation (device is already paired as secondary device). Ignoring.'
+    } else if (!valid) {
+      window.log.warn(
+        'Received invalid pairing authorisation for self. Could not verify signature. Ignoring.'
+      );
+    } else {
+      const { type, primaryDevicePubKey } = pairingAuthorisation;
+      if (type === textsecure.protobuf.PairingAuthorisationMessage.Type.GRANT) {
+        // Authorisation received to become a secondary device
+        window.log.info(
+          `Received pairing authorisation from ${primaryDevicePubKey}`
         );
-        return;
-      }
-      await libloki.storage.savePairingAuthorisation(pairingAuthorisation);
-      // Set current device as secondary.
-      // This will ensure the authorisation is sent
-      // along with each friend request.
-      window.storage.remove('secondaryDeviceStatus');
-      window.storage.put('isSecondaryDevice', true);
-      Whisper.events.trigger('secondaryDeviceRegistration');
-      // Update profile name
-      if (dataMessage && dataMessage.profile) {
-        const ourNumber = textsecure.storage.user.getNumber();
-        const me = window.ConversationController.get(ourNumber);
-        if (me) {
-          me.setLokiProfile(dataMessage.profile);
+        await libloki.storage.savePairingAuthorisation(pairingAuthorisation);
+        // Set current device as secondary.
+        // This will ensure the authorisation is sent
+        // along with each friend request.
+        window.storage.remove('secondaryDeviceStatus');
+        window.storage.put('isSecondaryDevice', true);
+        Whisper.events.trigger('secondaryDeviceRegistration');
+        // Update profile name
+        if (dataMessage && dataMessage.profile) {
+          const ourNumber = textsecure.storage.user.getNumber();
+          const me = window.ConversationController.get(ourNumber);
+          if (me) {
+            me.setLokiProfile(dataMessage.profile);
+          }
+        }
+        // Update contact list
+        if (syncMessage && syncMessage.contacts) {
+          // This call already removes the envelope from the cache
+          await this.handleContacts(envelope, syncMessage.contacts);
+          removedFromCache = true;
         }
+      } else {
+        window.log.warn('Unimplemented pairing authorisation message type');
       }
-    } else {
-      window.log.warn('Unimplemented pairing authorisation message type');
+    }
+    if (!removedFromCache) {
+      await this.removeFromCache(envelope);
     }
   },
-  async handleAuthorisationForContact(pairingAuthorisation) {
+  async handleAuthorisationForContact(envelope, pairingAuthorisation) {
     const valid = await this.validateAuthorisation(pairingAuthorisation);
     if (!valid) {
-      return;
-    }
-    const { primaryDevicePubKey, secondaryDevicePubKey } = pairingAuthorisation;
-    // ensure the primary device is a friend
-    const c = window.ConversationController.get(primaryDevicePubKey);
-    if (!c || !c.isFriend()) {
-      return;
+      window.log.warn(
+        'Received invalid pairing authorisation for self. Could not verify signature. Ignoring.'
+      );
+    } else {
+      const {
+        primaryDevicePubKey,
+        secondaryDevicePubKey,
+      } = pairingAuthorisation;
+      // ensure the primary device is a friend
+      const c = window.ConversationController.get(primaryDevicePubKey);
+      if (c && c.isFriend()) {
+        await libloki.storage.savePairingAuthorisation(pairingAuthorisation);
+        // send friend accept?
+        window.libloki.api.sendBackgroundMessage(secondaryDevicePubKey);
+      }
     }
-    await libloki.storage.savePairingAuthorisation(pairingAuthorisation);
-    // send friend accept?
-    window.libloki.api.sendBackgroundMessage(secondaryDevicePubKey);
+    return this.removeFromCache(envelope);
   },
-  async handlePairingAuthorisationMessage(
-    envelope,
-    { pairingAuthorisation, dataMessage }
-  ) {
+  async handlePairingAuthorisationMessage(envelope, content) {
+    const { pairingAuthorisation } = content;
     const { type, secondaryDevicePubKey } = pairingAuthorisation;
     if (type === textsecure.protobuf.PairingAuthorisationMessage.Type.REQUEST) {
-      await this.handlePairingRequest(pairingAuthorisation);
+      return this.handlePairingRequest(envelope, pairingAuthorisation);
     } else if (secondaryDevicePubKey === textsecure.storage.user.getNumber()) {
-      await this.handleAuthorisationForSelf(pairingAuthorisation, dataMessage);
-    } else {
-      await this.handleAuthorisationForContact(pairingAuthorisation);
+      return this.handleAuthorisationForSelf(
+        envelope,
+        pairingAuthorisation,
+        content
+      );
     }
-    return this.removeFromCache(envelope);
+    return this.handleAuthorisationForContact(envelope, pairingAuthorisation);
   },
   handleDataMessage(envelope, msg) {
     if (!envelope.isP2p) {
@@ -1455,11 +1476,11 @@ MessageReceiver.prototype.extend({
   },
   handleContacts(envelope, contacts) {
     window.log.info('contact sync');
-    const { blob } = contacts;
+    // const { blob } = contacts;
 
     // Note: we do not return here because we don't want to block the next message on
     //   this attachment download and a lot of processing of that attachment.
-    this.handleAttachment(blob).then(attachmentPointer => {
+    this.handleAttachment(contacts).then(attachmentPointer => {
       const results = [];
       const contactBuffer = new ContactBuffer(attachmentPointer.data);
       let contactDetails = contactBuffer.next();
@@ -1562,8 +1583,8 @@ MessageReceiver.prototype.extend({
     };
   },
   async downloadAttachment(attachment) {
-    window.log.info('Not downloading attachments.');
-    return Promise.reject();
+    // window.log.info('Not downloading attachments.');
+    // return Promise.reject();
 
     const encrypted = await this.server.getAttachment(attachment.id);
     const { key, digest, size } = attachment;
@@ -1588,8 +1609,11 @@ MessageReceiver.prototype.extend({
     };
   },
   handleAttachment(attachment) {
-    window.log.info('Not handling attachments.');
-    return Promise.reject();
+    // window.log.info('Not handling attachments.');
+    return Promise.resolve({
+      ...attachment,
+      data: dcodeIO.ByteBuffer.wrap(attachment.data).toArrayBuffer(), // ByteBuffer to ArrayBuffer
+    });
 
     const cleaned = this.cleanAttachment(attachment);
     return this.downloadAttachment(cleaned);
diff --git a/protos/SignalService.proto b/protos/SignalService.proto
index 439166e8e..bfb5a362e 100644
--- a/protos/SignalService.proto
+++ b/protos/SignalService.proto
@@ -272,6 +272,7 @@ message SyncMessage {
   message Contacts {
     optional AttachmentPointer blob     = 1;
     optional bool              complete = 2 [default = false];
+    optional bytes             data     = 101;
   }
 
   message Groups {
@@ -365,6 +366,7 @@ message ContactDetails {
   optional bytes    profileKey  = 6;
   optional bool     blocked     = 7;
   optional uint32   expireTimer = 8;
+  optional string   nickname    = 101;
 }
 
 message GroupDetails {