/* global dcodeIO, window, log, textsecure */ /* global storage: false */ /* global Signal: false */ /* global log: false */ const LokiAppDotNetAPI = require('./loki_app_dot_net_api'); const DEVICE_MAPPING_ANNOTATION_KEY = 'network.loki.messenger.devicemapping'; /* // returns the LokiFileServerAPI constructor with the serverUrl already consumed function LokiFileServerAPIWrapper(serverUrl) { return LokiFileServerAPI.bind(null, serverUrl); } */ // can have multiple of these objects instances as each user can have a // different home server class LokiFileServerAPI { constructor(ourKey) { this.ourKey = ourKey; this._adnApi = new LokiAppDotNetAPI(ourKey); } async establishConnection(serverUrl) { 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_ANNOTATION_KEY ); return deviceMapping ? deviceMapping.value : null; } 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); } async getDeviceMappingForUsers(pubKeys) { const users = await this._server.getUsers(pubKeys); return users; } async verifyUserObjectDeviceMap(pubKeys, isRequest, iterator) { const users = await this.getDeviceMappingForUsers(pubKeys); // log.info('verifyUserObjectDeviceMap Found', users.length, 'users') // go through each user and find deviceMap annotations const notFoundUsers = []; users.forEach(user => { let found = false; if (!user.annotations || !user.annotations.length) { log.info( `verifyUserObjectDeviceMap no annotation for ${user.username}` ); return; } user.annotations.forEach(note => { if (note.type !== 'network.loki.messenger.devicemapping') { return; } // isn't desired type // request is slave => primary type... if ( (isRequest && note.value.isPrimary !== '0') || (!isRequest && note.value.isPrimary === '0') ) { /* log.info(`verifyUserObjectDeviceMap found wrong type of` + `relationship ${user.username}`); */ // console.log(`https://file.lokinet.org/users/@${user.username}?prettyPrint=1&include_annotations=1`); return; } const { authorisations } = note.value; if (!Array.isArray(authorisations)) { return; } authorisations.forEach(auth => { // log.info('devmap auth', 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; } // log.info('auth', auth); try { // request (secondary wants to be paired with this primary) // grant (primary approves this secondary) window.libloki.crypto.verifyPairingSignature( auth.primaryDevicePubKey, auth.secondaryDevicePubKey, dcodeIO.ByteBuffer.wrap( isRequest ? auth.requestSignature : auth.grantSignature, 'base64' ).toArrayBuffer(), isRequest ? textsecure.protobuf.PairingAuthorisationMessage.Type.REQUEST : textsecure.protobuf.PairingAuthorisationMessage.Type.GRANT ); // log.info('auth is valid for', user.username) if (iterator(user.username, auth)) { found = true; } } catch (e) { log.warn( `Invalid signature on pubkey ${user.username} authorization ${ auth.secondaryDevicePubKey } isRequest ${isRequest}` ); } }); // end forEach authorisations }); // end forEach annotations if (!found) { notFoundUsers.push(user.username); } }); // end forEach 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 const checkSigs = {}; // cache for authorization const primaryPubKeys = []; // go through multiDeviceResults and get primary Pubkey await this.verifyUserObjectDeviceMap(pubKeys, true, (slaveKey, auth) => { // log.info('slave iterator', slaveKey); // if it doesn't throw, that means it's valid // add map to newSlavePrimaryMap if ( newSlavePrimaryMap[slaveKey] && newSlavePrimaryMap[slaveKey] !== auth.primaryDevicePubKey ) { log.warn( `file server user annotation primaryKey mismatch, had ${ newSlavePrimaryMap[slaveKey] } now ${auth.primaryDevicePubKey} for ${slaveKey}` ); return; } // log.info('valid', slaveKey); if (primaryPubKeys.indexOf(`@${auth.primaryDevicePubKey}`) === -1) { primaryPubKeys.push(`@${auth.primaryDevicePubKey}`); } checkSigs[slaveKey] = auth; newSlavePrimaryMap[slaveKey] = auth.primaryDevicePubKey; }); // end verifyUserObjectDeviceMap // log.info('verifyUserObjectDeviceMap', pubKeys, '=>', primaryPubKeys); // no valid primary pubkeys to check if (!primaryPubKeys.length) { // log.warn(`no valid primary pubkeys to check ${pubKeys}`); return []; } const verifiedPrimaryPKs = []; // get a list of all of primary pubKeys to verify the secondaryDevice assertion const notFoundUsers = await this.verifyUserObjectDeviceMap( primaryPubKeys, false, (primaryKey, auth) => { // log.info('primary iterator', slaveKey); if (verifiedPrimaryPKs.indexOf(`@${primaryKey}`) === -1) { verifiedPrimaryPKs.push(`@${primaryKey}`); } // assuming both are ordered // make sure our secondary and primary authorization match if ( JSON.stringify(checkSigs[auth.secondaryDevicePubKey]) !== JSON.stringify(auth) ) { // should hopefully never happen 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]; } }); }); // make new map final window.lokiPublicChatAPI.slavePrimaryMap = newSlavePrimaryMap; log.info( `Updated device mappings ${JSON.stringify( window.lokiPublicChatAPI.slavePrimaryMap )}` ); return verifiedPrimaryPKs; } _setOurDeviceMapping(authorisations, isPrimary) { const content = { isPrimary: isPrimary ? '1' : '0', authorisations, }; return this._server.setSelfAnnotation( DEVICE_MAPPING_ANNOTATION_KEY, content ); } uploadPrivateAttachment(data) { return this._server.uploadData(data); } } module.exports = LokiFileServerAPI;