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.
		
		
		
		
		
			
		
			
				
	
	
		
			279 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			279 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			JavaScript
		
	
/* global log, libloki */
 | 
						|
/* global storage: false */
 | 
						|
/* global Signal: false */
 | 
						|
/* global log: false */
 | 
						|
 | 
						|
const LokiAppDotNetAPI = require('./loki_app_dot_net_api');
 | 
						|
 | 
						|
const DEVICE_MAPPING_USER_ANNOTATION_TYPE =
 | 
						|
  'network.loki.messenger.devicemapping';
 | 
						|
 | 
						|
// can have multiple of these instances as each user can have a
 | 
						|
// different home server
 | 
						|
class LokiFileServerInstance {
 | 
						|
  constructor(ourKey) {
 | 
						|
    this.ourKey = ourKey;
 | 
						|
    // why don't we extend this?
 | 
						|
    this._adnApi = new LokiAppDotNetAPI(ourKey);
 | 
						|
    this.avatarMap = {};
 | 
						|
  }
 | 
						|
 | 
						|
  // FIXME: this is not file-server specific
 | 
						|
  // and is currently called by LokiAppDotNetAPI.
 | 
						|
  // LokiAppDotNetAPI (base) should not know about LokiFileServer.
 | 
						|
  async establishConnection(serverUrl) {
 | 
						|
    // FIXME: we don't always need a token...
 | 
						|
    this._server = await this._adnApi.findOrCreateServer(serverUrl);
 | 
						|
    // TODO: Handle this failure gracefully
 | 
						|
    if (!this._server) {
 | 
						|
      log.error('Failed to establish connection to file server');
 | 
						|
    }
 | 
						|
  }
 | 
						|
  async getUserDeviceMapping(pubKey) {
 | 
						|
    const annotations = await this._server.getUserAnnotations(pubKey);
 | 
						|
    const deviceMapping = annotations.find(
 | 
						|
      annotation => annotation.type === DEVICE_MAPPING_USER_ANNOTATION_TYPE
 | 
						|
    );
 | 
						|
    return deviceMapping ? deviceMapping.value : null;
 | 
						|
  }
 | 
						|
 | 
						|
  async verifyUserObjectDeviceMap(pubKeys, isRequest, iterator) {
 | 
						|
    const users = await this._server.getUsers(pubKeys);
 | 
						|
 | 
						|
    // go through each user and find deviceMap annotations
 | 
						|
    const notFoundUsers = [];
 | 
						|
    await Promise.all(
 | 
						|
      users.map(async user => {
 | 
						|
        let found = false;
 | 
						|
        // if this user has an avatar set, copy it into the map
 | 
						|
        this.avatarMap[user.username] = user.avatar_image
 | 
						|
          ? user.avatar_image.url
 | 
						|
          : false;
 | 
						|
        if (!user.annotations || !user.annotations.length) {
 | 
						|
          log.info(
 | 
						|
            `verifyUserObjectDeviceMap no annotation for ${user.username}`
 | 
						|
          );
 | 
						|
          return;
 | 
						|
        }
 | 
						|
        const mappingNote = user.annotations.find(
 | 
						|
          note => note.type === DEVICE_MAPPING_USER_ANNOTATION_TYPE
 | 
						|
        );
 | 
						|
        const { authorisations } = mappingNote.value;
 | 
						|
        if (!Array.isArray(authorisations)) {
 | 
						|
          return;
 | 
						|
        }
 | 
						|
        await Promise.all(
 | 
						|
          authorisations.map(async auth => {
 | 
						|
            // only skip, if in secondary search mode
 | 
						|
            if (isRequest && auth.secondaryDevicePubKey !== user.username) {
 | 
						|
              // this is not the authorization we're looking for
 | 
						|
              log.info(
 | 
						|
                `Request and ${auth.secondaryDevicePubKey} != ${user.username}`
 | 
						|
              );
 | 
						|
              return;
 | 
						|
            }
 | 
						|
            const valid = await libloki.crypto.validateAuthorisation(auth);
 | 
						|
            if (valid && iterator(user.username, auth)) {
 | 
						|
              found = true;
 | 
						|
            }
 | 
						|
          })
 | 
						|
        ); // end map authorisations
 | 
						|
 | 
						|
        if (!found) {
 | 
						|
          notFoundUsers.push(user.username);
 | 
						|
        }
 | 
						|
      })
 | 
						|
    ); // end map users
 | 
						|
    // log.info('done with users', users.length);
 | 
						|
    return notFoundUsers;
 | 
						|
  }
 | 
						|
 | 
						|
  // verifies list of pubKeys for any deviceMappings
 | 
						|
  // returns the relevant primary pubKeys
 | 
						|
  async verifyPrimaryPubKeys(pubKeys) {
 | 
						|
    const newSlavePrimaryMap = {}; // new slave to primary map
 | 
						|
    // checkSig disabled for now
 | 
						|
    // const checkSigs = {}; // cache for authorisation
 | 
						|
    const primaryPubKeys = [];
 | 
						|
    const result = {
 | 
						|
      verifiedPrimaryPKs: [],
 | 
						|
      slaveMap: {},
 | 
						|
    };
 | 
						|
 | 
						|
    // go through multiDeviceResults and get primary Pubkey
 | 
						|
    await this.verifyUserObjectDeviceMap(pubKeys, true, (slaveKey, auth) => {
 | 
						|
      // if we already have this key for a different device
 | 
						|
      if (
 | 
						|
        newSlavePrimaryMap[slaveKey] &&
 | 
						|
        newSlavePrimaryMap[slaveKey] !== auth.primaryDevicePubKey
 | 
						|
      ) {
 | 
						|
        log.warn(
 | 
						|
          `file server user annotation primaryKey mismatch, had ${
 | 
						|
            newSlavePrimaryMap[slaveKey]
 | 
						|
          } now ${auth.primaryDevicePubKey} for ${slaveKey}`
 | 
						|
        );
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      // at this point it's valid
 | 
						|
 | 
						|
      // add to primaryPubKeys
 | 
						|
      if (primaryPubKeys.indexOf(`@${auth.primaryDevicePubKey}`) === -1) {
 | 
						|
        primaryPubKeys.push(`@${auth.primaryDevicePubKey}`);
 | 
						|
      }
 | 
						|
 | 
						|
      // add authorisation cache
 | 
						|
      /*
 | 
						|
      if (checkSigs[`${auth.primaryDevicePubKey}_${slaveKey}`] !== undefined) {
 | 
						|
        log.warn(
 | 
						|
          `file server ${auth.primaryDevicePubKey} to ${slaveKey} double signed`
 | 
						|
        );
 | 
						|
      }
 | 
						|
      checkSigs[`${auth.primaryDevicePubKey}_${slaveKey}`] = auth;
 | 
						|
      */
 | 
						|
 | 
						|
      // add map to newSlavePrimaryMap
 | 
						|
      newSlavePrimaryMap[slaveKey] = auth.primaryDevicePubKey;
 | 
						|
    }); // end verifyUserObjectDeviceMap
 | 
						|
 | 
						|
    // no valid primary pubkeys to check
 | 
						|
    if (!primaryPubKeys.length) {
 | 
						|
      // log.warn(`no valid primary pubkeys to check ${pubKeys}`);
 | 
						|
      // do we want to update slavePrimaryMap?
 | 
						|
      return result;
 | 
						|
    }
 | 
						|
 | 
						|
    const verifiedPrimaryPKs = [];
 | 
						|
 | 
						|
    // get a list of all of primary pubKeys to verify the secondaryDevice assertion
 | 
						|
    const notFoundUsers = await this.verifyUserObjectDeviceMap(
 | 
						|
      primaryPubKeys,
 | 
						|
      false,
 | 
						|
      primaryKey => {
 | 
						|
        // add to verified list if we don't already have it
 | 
						|
        if (verifiedPrimaryPKs.indexOf(`@${primaryKey}`) === -1) {
 | 
						|
          verifiedPrimaryPKs.push(`@${primaryKey}`);
 | 
						|
        }
 | 
						|
 | 
						|
        // assuming both are ordered the same way
 | 
						|
        // make sure our secondary and primary authorization match
 | 
						|
        /*
 | 
						|
        if (
 | 
						|
          JSON.stringify(checkSigs[
 | 
						|
            `${auth.primaryDevicePubKey}_${auth.secondaryDevicePubKey}`
 | 
						|
          ]) !== JSON.stringify(auth)
 | 
						|
        ) {
 | 
						|
          // should hopefully never happen
 | 
						|
          // it did, old pairing data, I think...
 | 
						|
          log.warn(
 | 
						|
            `Valid authorizations from ${
 | 
						|
              auth.secondaryDevicePubKey
 | 
						|
            } does not match ${primaryKey}`
 | 
						|
          );
 | 
						|
          return false;
 | 
						|
        }
 | 
						|
        */
 | 
						|
        return true;
 | 
						|
      }
 | 
						|
    ); // end verifyUserObjectDeviceMap
 | 
						|
 | 
						|
    // remove from newSlavePrimaryMap if no valid mapping is found
 | 
						|
    notFoundUsers.forEach(primaryPubKey => {
 | 
						|
      Object.keys(newSlavePrimaryMap).forEach(slaveKey => {
 | 
						|
        if (newSlavePrimaryMap[slaveKey] === primaryPubKey) {
 | 
						|
          log.warn(
 | 
						|
            `removing unverifible ${slaveKey} to ${primaryPubKey} mapping`
 | 
						|
          );
 | 
						|
          delete newSlavePrimaryMap[slaveKey];
 | 
						|
        }
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    log.info(`Updated device mappings ${JSON.stringify(newSlavePrimaryMap)}`);
 | 
						|
 | 
						|
    result.verifiedPrimaryPKs = verifiedPrimaryPKs;
 | 
						|
    result.slaveMap = newSlavePrimaryMap;
 | 
						|
    return result;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
// extends LokiFileServerInstance with functions we'd only perform on our own home server
 | 
						|
// so we don't accidentally send info to the wrong file server
 | 
						|
class LokiHomeServerInstance extends LokiFileServerInstance {
 | 
						|
  _setOurDeviceMapping(authorisations, isPrimary) {
 | 
						|
    const content = {
 | 
						|
      isPrimary: isPrimary ? '1' : '0',
 | 
						|
      authorisations,
 | 
						|
    };
 | 
						|
    return this._server.setSelfAnnotation(
 | 
						|
      DEVICE_MAPPING_USER_ANNOTATION_TYPE,
 | 
						|
      content
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  async updateOurDeviceMapping() {
 | 
						|
    const isPrimary = !storage.get('isSecondaryDevice');
 | 
						|
    let authorisations;
 | 
						|
    if (isPrimary) {
 | 
						|
      authorisations = await Signal.Data.getGrantAuthorisationsForPrimaryPubKey(
 | 
						|
        this.ourKey
 | 
						|
      );
 | 
						|
    } else {
 | 
						|
      authorisations = [
 | 
						|
        await Signal.Data.getGrantAuthorisationForSecondaryPubKey(this.ourKey),
 | 
						|
      ];
 | 
						|
    }
 | 
						|
    return this._setOurDeviceMapping(authorisations, isPrimary);
 | 
						|
  }
 | 
						|
 | 
						|
  uploadAvatar(data) {
 | 
						|
    return this._server.uploadAvatar(data);
 | 
						|
  }
 | 
						|
 | 
						|
  uploadPrivateAttachment(data) {
 | 
						|
    return this._server.uploadData(data);
 | 
						|
  }
 | 
						|
 | 
						|
  clearOurDeviceMappingAnnotations() {
 | 
						|
    return this._server.setSelfAnnotation(
 | 
						|
      DEVICE_MAPPING_USER_ANNOTATION_TYPE,
 | 
						|
      null
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
// this will be our instance factory
 | 
						|
class LokiFileServerFactoryAPI {
 | 
						|
  constructor(ourKey) {
 | 
						|
    this.ourKey = ourKey;
 | 
						|
    this.servers = [];
 | 
						|
  }
 | 
						|
 | 
						|
  async establishHomeConnection(serverUrl) {
 | 
						|
    let thisServer = this.servers.find(
 | 
						|
      server => server._server.baseServerUrl === serverUrl
 | 
						|
    );
 | 
						|
    if (!thisServer) {
 | 
						|
      thisServer = new LokiHomeServerInstance(this.ourKey);
 | 
						|
      log.info(`Registering HomeServer ${serverUrl}`);
 | 
						|
      await thisServer.establishConnection(serverUrl);
 | 
						|
      this.servers.push(thisServer);
 | 
						|
    }
 | 
						|
    return thisServer;
 | 
						|
  }
 | 
						|
 | 
						|
  async establishConnection(serverUrl) {
 | 
						|
    let thisServer = this.servers.find(
 | 
						|
      server => server._server.baseServerUrl === serverUrl
 | 
						|
    );
 | 
						|
    if (!thisServer) {
 | 
						|
      thisServer = new LokiFileServerInstance(this.ourKey);
 | 
						|
      log.info(`Registering FileServer ${serverUrl}`);
 | 
						|
      await thisServer.establishConnection(serverUrl);
 | 
						|
      this.servers.push(thisServer);
 | 
						|
    }
 | 
						|
    return thisServer;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
module.exports = LokiFileServerFactoryAPI;
 |