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.
		
		
		
		
		
			
		
			
				
	
	
		
			311 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			311 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			JavaScript
		
	
/* global log, libloki, window, dcodeIO */
 | 
						|
/* global storage: 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;
 | 
						|
  }
 | 
						|
 | 
						|
  // FIXME: this is not file-server specific
 | 
						|
  // and is currently called by LokiAppDotNetAPI.
 | 
						|
  // LokiAppDotNetAPI (base) should not know about LokiFileServer.
 | 
						|
  async establishConnection(serverUrl, options) {
 | 
						|
    // why don't we extend this?
 | 
						|
    this._server = new LokiAppDotNetAPI(this.ourKey, serverUrl);
 | 
						|
 | 
						|
    // make sure pubKey & pubKeyHex are set in _server
 | 
						|
    this.pubKey = this._server.getPubKeyForUrl();
 | 
						|
 | 
						|
    if (options !== undefined && options.skipToken) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // get a token for multidevice
 | 
						|
    const gotToken = await this._server.getOrRefreshServerToken();
 | 
						|
    // TODO: Handle this failure gracefully
 | 
						|
    if (!gotToken) {
 | 
						|
      log.error('You are blacklisted form this home 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 (!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;
 | 
						|
        }
 | 
						|
        const validAuthorisations = authorisations.filter(
 | 
						|
          a => a && typeof a === 'object'
 | 
						|
        );
 | 
						|
        await Promise.all(
 | 
						|
          validAuthorisations.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 unverifiable ${slaveKey} to ${primaryPubKey} mapping`
 | 
						|
          );
 | 
						|
          delete newSlavePrimaryMap[slaveKey];
 | 
						|
        }
 | 
						|
      });
 | 
						|
    });
 | 
						|
 | 
						|
    log.info(`Updated device mappings ${JSON.stringify(newSlavePrimaryMap)}`);
 | 
						|
 | 
						|
    result.verifiedPrimaryPKs = verifiedPrimaryPKs;
 | 
						|
    result.slaveMap = newSlavePrimaryMap;
 | 
						|
    return result;
 | 
						|
  }
 | 
						|
 | 
						|
  // for files
 | 
						|
  async downloadAttachment(url) {
 | 
						|
    return this._server.downloadAttachment(url);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
// 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,
 | 
						|
    };
 | 
						|
    if (!this._server.token) {
 | 
						|
      log.warn('_setOurDeviceMapping no token yet');
 | 
						|
    }
 | 
						|
    return this._server.setSelfAnnotation(
 | 
						|
      DEVICE_MAPPING_USER_ANNOTATION_TYPE,
 | 
						|
      content
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  async updateOurDeviceMapping() {
 | 
						|
    if (!window.lokiFeatureFlags.useMultiDevice) {
 | 
						|
      return undefined;
 | 
						|
    }
 | 
						|
 | 
						|
    const isPrimary = !storage.get('isSecondaryDevice');
 | 
						|
    const authorisations = await window.libsession.Protocols.MultiDeviceProtocol.getPairingAuthorisations(
 | 
						|
      this.ourKey
 | 
						|
    );
 | 
						|
 | 
						|
    const authorisationsBase64 = authorisations.map(authorisation => {
 | 
						|
      const requestSignature = dcodeIO.ByteBuffer.wrap(
 | 
						|
        authorisation.requestSignature
 | 
						|
      ).toString('base64');
 | 
						|
      const grantSignature = authorisation.grantSignature
 | 
						|
        ? dcodeIO.ByteBuffer.wrap(authorisation.grantSignature).toString(
 | 
						|
            'base64'
 | 
						|
          )
 | 
						|
        : null;
 | 
						|
      return {
 | 
						|
        ...authorisation,
 | 
						|
        requestSignature,
 | 
						|
        grantSignature,
 | 
						|
      };
 | 
						|
    });
 | 
						|
 | 
						|
    return this._setOurDeviceMapping(authorisationsBase64, isPrimary);
 | 
						|
  }
 | 
						|
 | 
						|
  // you only upload to your own home server
 | 
						|
  // you can download from any server...
 | 
						|
  uploadAvatar(data) {
 | 
						|
    if (!this._server.token) {
 | 
						|
      log.warn('uploadAvatar no token yet');
 | 
						|
    }
 | 
						|
    return this._server.uploadAvatar(data);
 | 
						|
  }
 | 
						|
 | 
						|
  static uploadPrivateAttachment(data) {
 | 
						|
    return window.tokenlessFileServerAdnAPI.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 = [];
 | 
						|
  }
 | 
						|
 | 
						|
  establishHomeConnection(serverUrl) {
 | 
						|
    let thisServer = this.servers.find(
 | 
						|
      server => server._server.baseServerUrl === serverUrl
 | 
						|
    );
 | 
						|
    if (!thisServer) {
 | 
						|
      thisServer = new LokiHomeServerInstance(this.ourKey);
 | 
						|
      log.info(`Registering HomeServer ${serverUrl}`);
 | 
						|
      // not await, so a failure or slow connection doesn't hinder loading of the app
 | 
						|
      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, { skipToken: true });
 | 
						|
      this.servers.push(thisServer);
 | 
						|
    }
 | 
						|
    return thisServer;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
module.exports = LokiFileServerFactoryAPI;
 |