You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			404 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			404 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
/* global window, libsignal, textsecure, Signal,
 | 
						|
   lokiFileServerAPI, ConversationController */
 | 
						|
 | 
						|
// eslint-disable-next-line func-names
 | 
						|
(function() {
 | 
						|
  window.libloki = window.libloki || {};
 | 
						|
 | 
						|
  const timers = {};
 | 
						|
  const REFRESH_DELAY = 60 * 1000;
 | 
						|
 | 
						|
  async function getPreKeyBundleForContact(pubKey) {
 | 
						|
    const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
 | 
						|
    const identityKey = myKeyPair.pubKey;
 | 
						|
 | 
						|
    // Retrieve ids. The ids stored are always the latest generated + 1
 | 
						|
    const signedKeyId = textsecure.storage.get('signedKeyId', 2) - 1;
 | 
						|
 | 
						|
    const [signedKey, preKey] = await Promise.all([
 | 
						|
      textsecure.storage.protocol.loadSignedPreKey(signedKeyId),
 | 
						|
      new Promise(async resolve => {
 | 
						|
        // retrieve existing prekey if we already generated one for that recipient
 | 
						|
        const storedPreKey = await textsecure.storage.protocol.loadPreKeyForContact(
 | 
						|
          pubKey
 | 
						|
        );
 | 
						|
        if (storedPreKey) {
 | 
						|
          resolve({ pubKey: storedPreKey.pubKey, keyId: storedPreKey.keyId });
 | 
						|
        } else {
 | 
						|
          // generate and store new prekey
 | 
						|
          const preKeyId = textsecure.storage.get('maxPreKeyId', 1);
 | 
						|
          textsecure.storage.put('maxPreKeyId', preKeyId + 1);
 | 
						|
          const newPreKey = await libsignal.KeyHelper.generatePreKey(preKeyId);
 | 
						|
          await textsecure.storage.protocol.storePreKey(
 | 
						|
            newPreKey.keyId,
 | 
						|
            newPreKey.keyPair,
 | 
						|
            pubKey
 | 
						|
          );
 | 
						|
          resolve({ pubKey: newPreKey.keyPair.pubKey, keyId: preKeyId });
 | 
						|
        }
 | 
						|
      }),
 | 
						|
    ]);
 | 
						|
 | 
						|
    return {
 | 
						|
      identityKey: new Uint8Array(identityKey),
 | 
						|
      deviceId: 1, // TODO: fetch from somewhere
 | 
						|
      preKeyId: preKey.keyId,
 | 
						|
      signedKeyId,
 | 
						|
      preKey: new Uint8Array(preKey.pubKey),
 | 
						|
      signedKey: new Uint8Array(signedKey.pubKey),
 | 
						|
      signature: new Uint8Array(signedKey.signature),
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  async function saveContactPreKeyBundle({
 | 
						|
    pubKey,
 | 
						|
    preKeyId,
 | 
						|
    preKey,
 | 
						|
    signedKeyId,
 | 
						|
    signedKey,
 | 
						|
    signature,
 | 
						|
  }) {
 | 
						|
    const signedPreKey = {
 | 
						|
      keyId: signedKeyId,
 | 
						|
      publicKey: signedKey,
 | 
						|
      signature,
 | 
						|
    };
 | 
						|
 | 
						|
    const signedKeyPromise = textsecure.storage.protocol.storeContactSignedPreKey(
 | 
						|
      pubKey,
 | 
						|
      signedPreKey
 | 
						|
    );
 | 
						|
 | 
						|
    const preKeyObject = {
 | 
						|
      publicKey: preKey,
 | 
						|
      keyId: preKeyId,
 | 
						|
    };
 | 
						|
 | 
						|
    const preKeyPromise = textsecure.storage.protocol.storeContactPreKey(
 | 
						|
      pubKey,
 | 
						|
      preKeyObject
 | 
						|
    );
 | 
						|
 | 
						|
    await Promise.all([signedKeyPromise, preKeyPromise]);
 | 
						|
  }
 | 
						|
 | 
						|
  async function removeContactPreKeyBundle(pubKey) {
 | 
						|
    await Promise.all([
 | 
						|
      textsecure.storage.protocol.removeContactPreKey(pubKey),
 | 
						|
      textsecure.storage.protocol.removeContactSignedPreKey(pubKey),
 | 
						|
    ]);
 | 
						|
  }
 | 
						|
 | 
						|
  async function verifyFriendRequestAcceptPreKey(pubKey, buffer) {
 | 
						|
    const storedPreKey = await textsecure.storage.protocol.loadPreKeyForContact(
 | 
						|
      pubKey
 | 
						|
    );
 | 
						|
    if (!storedPreKey) {
 | 
						|
      throw new Error(
 | 
						|
        'Received a friend request from a pubkey for which no prekey bundle was created'
 | 
						|
      );
 | 
						|
    }
 | 
						|
    // need to pop the version
 | 
						|
    // eslint-disable-next-line no-unused-vars
 | 
						|
    const version = buffer.readUint8();
 | 
						|
    const preKeyProto = window.textsecure.protobuf.PreKeyWhisperMessage.decode(
 | 
						|
      buffer
 | 
						|
    );
 | 
						|
    if (!preKeyProto) {
 | 
						|
      throw new Error(
 | 
						|
        'Could not decode PreKeyWhisperMessage while attempting to match the preKeyId'
 | 
						|
      );
 | 
						|
    }
 | 
						|
    const { preKeyId } = preKeyProto;
 | 
						|
    if (storedPreKey.keyId !== preKeyId) {
 | 
						|
      throw new Error(
 | 
						|
        'Received a preKeyWhisperMessage (friend request accept) from an unknown source'
 | 
						|
      );
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // fetches device mappings from server.
 | 
						|
  async function getPrimaryDeviceMapping(pubKey) {
 | 
						|
    if (typeof lokiFileServerAPI === 'undefined') {
 | 
						|
      // If this is not defined then we are initiating a pairing
 | 
						|
      return [];
 | 
						|
    }
 | 
						|
    const deviceMapping = await lokiFileServerAPI.getUserDeviceMapping(pubKey);
 | 
						|
    if (!deviceMapping) {
 | 
						|
      return [];
 | 
						|
    }
 | 
						|
    let authorisations = deviceMapping.authorisations || [];
 | 
						|
    if (deviceMapping.isPrimary === '0') {
 | 
						|
      const { primaryDevicePubKey } =
 | 
						|
        authorisations.find(
 | 
						|
          authorisation =>
 | 
						|
            authorisation && authorisation.secondaryDevicePubKey === pubKey
 | 
						|
        ) || {};
 | 
						|
      if (primaryDevicePubKey) {
 | 
						|
        // do NOT call getprimaryDeviceMapping recursively
 | 
						|
        // in case both devices are out of sync and think they are
 | 
						|
        // each others' secondary pubkey.
 | 
						|
        const primaryDeviceMapping = await lokiFileServerAPI.getUserDeviceMapping(
 | 
						|
          primaryDevicePubKey
 | 
						|
        );
 | 
						|
        if (!primaryDeviceMapping) {
 | 
						|
          return [];
 | 
						|
        }
 | 
						|
        ({ authorisations } = primaryDeviceMapping);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // filter out any invalid authorisations
 | 
						|
    return authorisations.filter(a => a && typeof a === 'object') || [];
 | 
						|
  }
 | 
						|
 | 
						|
  // if the device is a secondary device,
 | 
						|
  // fetch the device mappings for its primary device
 | 
						|
  async function saveAllPairingAuthorisationsFor(pubKey) {
 | 
						|
    // Will be false if there is no timer
 | 
						|
    const cacheValid = timers[pubKey] > Date.now();
 | 
						|
    if (cacheValid) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    timers[pubKey] = Date.now() + REFRESH_DELAY;
 | 
						|
    const authorisations = await getPrimaryDeviceMapping(pubKey);
 | 
						|
    await Promise.all(
 | 
						|
      authorisations.map(authorisation =>
 | 
						|
        savePairingAuthorisation(authorisation)
 | 
						|
      )
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  async function savePairingAuthorisation(authorisation) {
 | 
						|
    if (!authorisation) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Ensure that we have a conversation for all the devices
 | 
						|
    const conversation = await ConversationController.getOrCreateAndWait(
 | 
						|
      authorisation.secondaryDevicePubKey,
 | 
						|
      'private'
 | 
						|
    );
 | 
						|
    await conversation.setSecondaryStatus(
 | 
						|
      true,
 | 
						|
      authorisation.primaryDevicePubKey
 | 
						|
    );
 | 
						|
    await window.Signal.Data.createOrUpdatePairingAuthorisation(authorisation);
 | 
						|
  }
 | 
						|
 | 
						|
  function removePairingAuthorisationForSecondaryPubKey(pubKey) {
 | 
						|
    return window.Signal.Data.removePairingAuthorisationForSecondaryPubKey(
 | 
						|
      pubKey
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  // Transforms signatures from base64 to ArrayBuffer!
 | 
						|
  async function getGrantAuthorisationForSecondaryPubKey(secondaryPubKey) {
 | 
						|
    const conversation = ConversationController.get(secondaryPubKey);
 | 
						|
    if (!conversation || conversation.isPublic() || conversation.isRss()) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
    await saveAllPairingAuthorisationsFor(secondaryPubKey);
 | 
						|
    const authorisation = await window.Signal.Data.getGrantAuthorisationForSecondaryPubKey(
 | 
						|
      secondaryPubKey
 | 
						|
    );
 | 
						|
    if (!authorisation) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
    return {
 | 
						|
      ...authorisation,
 | 
						|
      requestSignature: Signal.Crypto.base64ToArrayBuffer(
 | 
						|
        authorisation.requestSignature
 | 
						|
      ),
 | 
						|
      grantSignature: Signal.Crypto.base64ToArrayBuffer(
 | 
						|
        authorisation.grantSignature
 | 
						|
      ),
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  // Transforms signatures from base64 to ArrayBuffer!
 | 
						|
  async function getAuthorisationForSecondaryPubKey(secondaryPubKey) {
 | 
						|
    await saveAllPairingAuthorisationsFor(secondaryPubKey);
 | 
						|
    const authorisation = await window.Signal.Data.getAuthorisationForSecondaryPubKey(
 | 
						|
      secondaryPubKey
 | 
						|
    );
 | 
						|
    if (!authorisation) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
    return {
 | 
						|
      ...authorisation,
 | 
						|
      requestSignature: Signal.Crypto.base64ToArrayBuffer(
 | 
						|
        authorisation.requestSignature
 | 
						|
      ),
 | 
						|
      grantSignature: authorisation.grantSignature
 | 
						|
        ? Signal.Crypto.base64ToArrayBuffer(authorisation.grantSignature)
 | 
						|
        : null,
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  function getSecondaryDevicesFor(primaryDevicePubKey) {
 | 
						|
    return window.Signal.Data.getSecondaryDevicesFor(primaryDevicePubKey);
 | 
						|
  }
 | 
						|
 | 
						|
  function getGuardNodes() {
 | 
						|
    return window.Signal.Data.getGuardNodes();
 | 
						|
  }
 | 
						|
 | 
						|
  function updateGuardNodes(nodes) {
 | 
						|
    return window.Signal.Data.updateGuardNodes(nodes);
 | 
						|
  }
 | 
						|
 | 
						|
  async function getAllDevicePubKeysForPrimaryPubKey(primaryDevicePubKey) {
 | 
						|
    await saveAllPairingAuthorisationsFor(primaryDevicePubKey);
 | 
						|
    const secondaryPubKeys =
 | 
						|
      (await getSecondaryDevicesFor(primaryDevicePubKey)) || [];
 | 
						|
    return secondaryPubKeys.concat(primaryDevicePubKey);
 | 
						|
  }
 | 
						|
 | 
						|
  function getPairedDevicesFor(pubkey) {
 | 
						|
    return window.Signal.Data.getPairedDevicesFor(pubkey);
 | 
						|
  }
 | 
						|
 | 
						|
  window.libloki.storage = {
 | 
						|
    getPreKeyBundleForContact,
 | 
						|
    saveContactPreKeyBundle,
 | 
						|
    removeContactPreKeyBundle,
 | 
						|
    verifyFriendRequestAcceptPreKey,
 | 
						|
    savePairingAuthorisation,
 | 
						|
    saveAllPairingAuthorisationsFor,
 | 
						|
    removePairingAuthorisationForSecondaryPubKey,
 | 
						|
    getGrantAuthorisationForSecondaryPubKey,
 | 
						|
    getAuthorisationForSecondaryPubKey,
 | 
						|
    getPairedDevicesFor,
 | 
						|
    getAllDevicePubKeysForPrimaryPubKey,
 | 
						|
    getSecondaryDevicesFor,
 | 
						|
    getPrimaryDeviceMapping,
 | 
						|
    getGuardNodes,
 | 
						|
    updateGuardNodes,
 | 
						|
  };
 | 
						|
 | 
						|
  // Libloki protocol store
 | 
						|
 | 
						|
  const store = window.SignalProtocolStore.prototype;
 | 
						|
 | 
						|
  store.storeContactPreKey = async (pubKey, preKey) => {
 | 
						|
    const key = {
 | 
						|
      // id: (autoincrement)
 | 
						|
      identityKeyString: pubKey,
 | 
						|
      publicKey: preKey.publicKey,
 | 
						|
      keyId: preKey.keyId,
 | 
						|
    };
 | 
						|
 | 
						|
    await window.Signal.Data.createOrUpdateContactPreKey(key);
 | 
						|
  };
 | 
						|
 | 
						|
  store.loadContactPreKey = async pubKey => {
 | 
						|
    const preKey = await window.Signal.Data.getContactPreKeyByIdentityKey(
 | 
						|
      pubKey
 | 
						|
    );
 | 
						|
    if (preKey) {
 | 
						|
      return {
 | 
						|
        id: preKey.id,
 | 
						|
        keyId: preKey.keyId,
 | 
						|
        publicKey: preKey.publicKey,
 | 
						|
        identityKeyString: preKey.identityKeyString,
 | 
						|
      };
 | 
						|
    }
 | 
						|
 | 
						|
    window.log.warn('Failed to fetch contact prekey:', pubKey);
 | 
						|
    return undefined;
 | 
						|
  };
 | 
						|
 | 
						|
  store.loadContactPreKeys = async filters => {
 | 
						|
    const { keyId, identityKeyString } = filters;
 | 
						|
    const keys = await window.Signal.Data.getContactPreKeys(
 | 
						|
      keyId,
 | 
						|
      identityKeyString
 | 
						|
    );
 | 
						|
    if (keys) {
 | 
						|
      return keys.map(preKey => ({
 | 
						|
        id: preKey.id,
 | 
						|
        keyId: preKey.keyId,
 | 
						|
        publicKey: preKey.publicKey,
 | 
						|
        identityKeyString: preKey.identityKeyString,
 | 
						|
      }));
 | 
						|
    }
 | 
						|
 | 
						|
    window.log.warn('Failed to fetch signed prekey with filters', filters);
 | 
						|
    return undefined;
 | 
						|
  };
 | 
						|
 | 
						|
  store.removeContactPreKey = async pubKey => {
 | 
						|
    await window.Signal.Data.removeContactPreKeyByIdentityKey(pubKey);
 | 
						|
  };
 | 
						|
 | 
						|
  store.clearContactPreKeysStore = async () => {
 | 
						|
    await window.Signal.Data.removeAllContactPreKeys();
 | 
						|
  };
 | 
						|
 | 
						|
  store.storeContactSignedPreKey = async (pubKey, signedPreKey) => {
 | 
						|
    const key = {
 | 
						|
      // id: (autoincrement)
 | 
						|
      identityKeyString: pubKey,
 | 
						|
      keyId: signedPreKey.keyId,
 | 
						|
      publicKey: signedPreKey.publicKey,
 | 
						|
      signature: signedPreKey.signature,
 | 
						|
      created_at: Date.now(),
 | 
						|
      confirmed: false,
 | 
						|
    };
 | 
						|
    await window.Signal.Data.createOrUpdateContactSignedPreKey(key);
 | 
						|
  };
 | 
						|
 | 
						|
  store.loadContactSignedPreKey = async pubKey => {
 | 
						|
    const preKey = await window.Signal.Data.getContactSignedPreKeyByIdentityKey(
 | 
						|
      pubKey
 | 
						|
    );
 | 
						|
    if (preKey) {
 | 
						|
      return {
 | 
						|
        id: preKey.id,
 | 
						|
        identityKeyString: preKey.identityKeyString,
 | 
						|
        publicKey: preKey.publicKey,
 | 
						|
        signature: preKey.signature,
 | 
						|
        created_at: preKey.created_at,
 | 
						|
        keyId: preKey.keyId,
 | 
						|
        confirmed: preKey.confirmed,
 | 
						|
      };
 | 
						|
    }
 | 
						|
    window.log.warn('Failed to fetch contact signed prekey:', pubKey);
 | 
						|
    return undefined;
 | 
						|
  };
 | 
						|
 | 
						|
  store.loadContactSignedPreKeys = async filters => {
 | 
						|
    const { keyId, identityKeyString } = filters;
 | 
						|
    const keys = await window.Signal.Data.getContactSignedPreKeys(
 | 
						|
      keyId,
 | 
						|
      identityKeyString
 | 
						|
    );
 | 
						|
    if (keys) {
 | 
						|
      return keys.map(preKey => ({
 | 
						|
        id: preKey.id,
 | 
						|
        identityKeyString: preKey.identityKeyString,
 | 
						|
        publicKey: preKey.publicKey,
 | 
						|
        signature: preKey.signature,
 | 
						|
        created_at: preKey.created_at,
 | 
						|
        keyId: preKey.keyId,
 | 
						|
        confirmed: preKey.confirmed,
 | 
						|
      }));
 | 
						|
    }
 | 
						|
 | 
						|
    window.log.warn(
 | 
						|
      'Failed to fetch contact signed prekey with filters',
 | 
						|
      filters
 | 
						|
    );
 | 
						|
    return undefined;
 | 
						|
  };
 | 
						|
 | 
						|
  store.removeContactSignedPreKey = async pubKey => {
 | 
						|
    await window.Signal.Data.removeContactSignedPreKeyByIdentityKey(pubKey);
 | 
						|
  };
 | 
						|
 | 
						|
  store.clearContactSignedPreKeysStore = async () => {
 | 
						|
    await window.Signal.Data.removeAllContactSignedPreKeys();
 | 
						|
  };
 | 
						|
})();
 |