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;
 |