/* global log, libloki, process, window */ /* 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'; // const LOKIFOUNDATION_DEVFILESERVER_PUBKEY = // 'BSZiMVxOco/b3sYfaeyiMWv/JnqokxGXkHoclEx8TmZ6'; const LOKIFOUNDATION_FILESERVER_PUBKEY = 'BWJQnVm97sQE3Q1InB4Vuo+U/T1hmwHBv0ipkiv8tzEc'; // can have multiple of these instances as each user can have a // different home server class LokiFileServerInstance { constructor(ourKey) { this.ourKey = ourKey; // do we have their pubkey locally? /* // get remote pubKey this._server.serverRequest('loki/v1/public_key').then(keyRes => { // we don't need to delay to protect identity because the token request // should only be done over lokinet-lite this.delayToken = true; if (keyRes.err || !keyRes.response || !keyRes.response.data) { if (keyRes.err) { log.error(`Error ${keyRes.err}`); } } else { // store it this.pubKey = dcodeIO.ByteBuffer.wrap( keyRes.response.data, 'base64' ).toArrayBuffer(); // write it to a file } }); */ // Hard coded this.pubKey = window.Signal.Crypto.base64ToArrayBuffer( LOKIFOUNDATION_FILESERVER_PUBKEY ); if (this.pubKey.byteLength && this.pubKey.byteLength !== 33) { log.error( 'FILESERVER PUBKEY is invalid, length:', this.pubKey.byteLength ); process.exit(1); } } // 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); // configure proxy this._server.pubKey = this.pubKey; 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; } 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, }; if (!this._server.token) { log.warn('_setOurDeviceMapping no token yet'); } 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); } // 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; } } // smuggle some data out of this joint (for expire.js/version upgrade check) LokiFileServerFactoryAPI.secureRpcPubKey = LOKIFOUNDATION_FILESERVER_PUBKEY; module.exports = LokiFileServerFactoryAPI;