Merge pull request #1176 from Mikunj/multi-device-protocol

Multidevice Protocol Refactor
This commit is contained in:
Mikunj Varsani 2020-06-17 11:26:25 +10:00 committed by GitHub
commit ce868456c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 405 additions and 451 deletions

View file

@ -74,12 +74,8 @@ module.exports = {
removeAllContactSignedPreKeys,
createOrUpdatePairingAuthorisation,
removePairingAuthorisationForSecondaryPubKey,
getAuthorisationForSecondaryPubKey,
getGrantAuthorisationsForPrimaryPubKey,
getSecondaryDevicesFor,
getPrimaryDeviceFor,
getPairedDevicesFor,
getPairingAuthorisationsFor,
removePairingAuthorisationsFor,
createOrUpdateItem,
getItemById,
@ -1506,42 +1502,19 @@ async function removeAllSignedPreKeys() {
const PAIRING_AUTHORISATIONS_TABLE = 'pairingAuthorisations';
const GUARD_NODE_TABLE = 'guardNodes';
async function getAuthorisationForSecondaryPubKey(pubKey, options) {
const granted = options && options.granted;
let filter = '';
if (granted) {
filter = 'AND isGranted = 1';
}
const row = await db.get(
`SELECT json FROM ${PAIRING_AUTHORISATIONS_TABLE} WHERE secondaryDevicePubKey = $secondaryDevicePubKey ${filter};`,
{
$secondaryDevicePubKey: pubKey,
}
);
if (!row) {
return null;
}
return jsonToObject(row.json);
}
async function getGrantAuthorisationsForPrimaryPubKey(primaryDevicePubKey) {
async function getPairingAuthorisationsFor(pubKey) {
const rows = await db.all(
`SELECT json FROM ${PAIRING_AUTHORISATIONS_TABLE} WHERE primaryDevicePubKey = $primaryDevicePubKey AND isGranted = 1 ORDER BY secondaryDevicePubKey ASC;`,
{
$primaryDevicePubKey: primaryDevicePubKey,
}
`SELECT json FROM ${PAIRING_AUTHORISATIONS_TABLE} WHERE primaryDevicePubKey = $pubKey OR secondaryDevicePubKey = $pubKey;`,
{ $pubKey: pubKey }
);
return map(rows, row => jsonToObject(row.json));
return rows.map(row => jsonToObject(row.json));
}
async function createOrUpdatePairingAuthorisation(data) {
const { primaryDevicePubKey, secondaryDevicePubKey, grantSignature } = data;
// remove any existing authorisation for this pubkey (we allow only one secondary device for now)
await removePairingAuthorisationForPrimaryPubKey(primaryDevicePubKey);
await removePairingAuthorisationsFor(primaryDevicePubKey);
await db.run(
`INSERT OR REPLACE INTO ${PAIRING_AUTHORISATIONS_TABLE} (
@ -1564,30 +1537,16 @@ async function createOrUpdatePairingAuthorisation(data) {
);
}
async function removePairingAuthorisationForPrimaryPubKey(pubKey) {
async function removePairingAuthorisationsFor(pubKey) {
await db.run(
`DELETE FROM ${PAIRING_AUTHORISATIONS_TABLE} WHERE primaryDevicePubKey = $primaryDevicePubKey;`,
`DELETE FROM ${PAIRING_AUTHORISATIONS_TABLE} WHERE primaryDevicePubKey = $pubKey OR secondaryDevicePubKey = $pubKey;`,
{
$primaryDevicePubKey: pubKey,
$pubKey: pubKey,
}
);
}
async function removePairingAuthorisationForSecondaryPubKey(pubKey) {
await db.run(
`DELETE FROM ${PAIRING_AUTHORISATIONS_TABLE} WHERE secondaryDevicePubKey = $secondaryDevicePubKey;`,
{
$secondaryDevicePubKey: pubKey,
}
);
}
async function getSecondaryDevicesFor(primaryDevicePubKey) {
const authorisations = await getGrantAuthorisationsForPrimaryPubKey(
primaryDevicePubKey
);
return map(authorisations, row => row.secondaryDevicePubKey);
}
const GUARD_NODE_TABLE = 'guardNodes';
async function getGuardNodes() {
const nodes = await db.all(`SELECT ed25519PubKey FROM ${GUARD_NODE_TABLE};`);
@ -1620,42 +1579,8 @@ async function updateGuardNodes(nodes) {
await db.run('END TRANSACTION;');
}
async function getPrimaryDeviceFor(secondaryDevicePubKey) {
const row = await db.get(
`SELECT primaryDevicePubKey FROM ${PAIRING_AUTHORISATIONS_TABLE} WHERE secondaryDevicePubKey = $secondaryDevicePubKey AND isGranted = 1;`,
{
$secondaryDevicePubKey: secondaryDevicePubKey,
}
);
if (!row) {
return null;
}
return row.primaryDevicePubKey;
}
// Return all the paired pubkeys for a specific pubkey (excluded),
// irrespective of their Primary or Secondary status.
async function getPairedDevicesFor(pubKey) {
let results = [];
// get primary pubkey (only works if the pubkey is a secondary pubkey)
const primaryPubKey = await getPrimaryDeviceFor(pubKey);
if (primaryPubKey) {
results.push(primaryPubKey);
}
// get secondary pubkeys (only works if the pubkey is a primary pubkey)
const secondaryPubKeys = await getSecondaryDevicesFor(
primaryPubKey || pubKey
);
results = results.concat(secondaryPubKeys);
// ensure the input pubkey is not in the results
results = results.filter(x => x !== pubKey);
return results;
}
const ITEMS_TABLE = 'items';
async function createOrUpdateItem(data) {

View file

@ -9,6 +9,7 @@
textsecure,
Whisper,
libloki,
libsession,
libsignal,
StringView,
BlockedNumberController,
@ -1425,7 +1426,7 @@
Whisper.events.on('devicePairingRequestRejected', async pubKey => {
await libloki.storage.removeContactPreKeyBundle(pubKey);
await libloki.storage.removePairingAuthorisationForSecondaryPubKey(
await libsession.Protocols.MultiDeviceProtocol.removePairingAuthorisations(
pubKey
);
});
@ -1435,8 +1436,7 @@
if (isSecondaryDevice) {
return;
}
await libloki.storage.removePairingAuthorisationForSecondaryPubKey(
await libsession.Protocols.MultiDeviceProtocol.removePairingAuthorisations(
pubKey
);
await window.lokiFileServerAPI.updateOurDeviceMapping();
@ -1761,16 +1761,15 @@
return;
}
let primaryDevice = null;
const authorisation = await libloki.storage.getGrantAuthorisationForSecondaryPubKey(
sender
);
if (authorisation) {
primaryDevice = authorisation.primaryDevicePubKey;
}
// A sender here could be referring to a group.
// Groups don't have primary devices so we need to take that into consideration.
const user = libsession.Types.PubKey.from(sender);
const primaryDevice = user
? await libsession.Protocols.MultiDeviceProtocol.getPrimaryDevice(user)
: null;
const conversation = ConversationController.get(
groupId || primaryDevice || sender
groupId || (primaryDevice && primaryDevice.key) || sender
);
if (conversation) {
@ -1826,25 +1825,21 @@
activeAt = activeAt || Date.now();
}
const ourPrimaryKey = window.storage.get('primaryDevicePubKey');
const ourDevices = await libloki.storage.getAllDevicePubKeysForPrimaryPubKey(
ourPrimaryKey
);
// TODO: We should probably just *not* send any secondary devices and
// just load them all and send FRs when we get the mapping
const isOurSecondaryDevice =
id !== ourPrimaryKey &&
ourDevices &&
ourDevices.some(devicePubKey => devicePubKey === id);
if (isOurSecondaryDevice) {
await conversation.setSecondaryStatus(true, ourPrimaryKey);
if (ourPrimaryKey) {
const secondaryDevices = await libsession.Protocols.MultiDeviceProtocol.getSecondaryDevices(
ourPrimaryKey
);
if (secondaryDevices.some(device => device.key === id)) {
await conversation.setSecondaryStatus(true, ourPrimaryKey);
}
}
const otherDevices = await libloki.storage.getPairedDevicesFor(id);
const devices = [id, ...otherDevices];
const devices = await libsession.Protocols.MultiDeviceProtocol.getAllDevices(
id
);
const deviceConversations = await Promise.all(
devices.map(d =>
ConversationController.getOrCreateAndWait(d, 'private')
ConversationController.getOrCreateAndWait(d.key, 'private')
)
);
deviceConversations.forEach(device => {

View file

@ -4,7 +4,6 @@
ConversationController,
MessageController,
_,
libloki,
*/
/* eslint-disable more/no-then */
@ -31,18 +30,15 @@
this.remove(receipts);
return receipts;
},
async getTargetMessage(source, messages) {
async getTargetMessage(originalSource, messages) {
if (messages.length === 0) {
return null;
}
const authorisation = await libloki.storage.getGrantAuthorisationForSecondaryPubKey(
source
const primary = await window.libsession.Protocols.MultiDeviceProtocol.getPrimaryDevice(
originalSource
);
if (authorisation) {
// eslint-disable-next-line no-param-reassign
source = authorisation.primaryDevicePubKey;
}
const source = primary.key;
const message = messages.find(
item => !item.isIncoming() && source === item.get('conversationId')

View file

@ -211,10 +211,9 @@
return true;
}
const ourDevices = await window.libloki.storage.getPairedDevicesFor(
this.ourNumber
return window.libsession.Protocols.MultiDeviceProtocol.isOurDevice(
this.id
);
return ourDevices.includes(this.id);
},
isOurLocalDevice() {
return this.id === this.ourNumber;
@ -876,15 +875,19 @@
// This is already the primary conversation
return this;
}
const authorisation = await window.libloki.storage.getAuthorisationForSecondaryPubKey(
this.id
);
if (authorisation) {
const device = window.libsession.Types.PubKey.from(this.id);
if (device) {
const primary = await window.libsession.Protocols.MultiDeviceProtocol.getPrimaryDevice(
device
);
return ConversationController.getOrCreateAndWait(
authorisation.primaryDevicePubKey,
primary.key,
'private'
);
}
// Something funky has happened
return this;
},

View file

@ -1,4 +1,3 @@
type MessageModelType = 'incoming' | 'outgoing' | 'friend-request';
export type EndSessionType = 'done' | 'ongoing';

21
js/modules/data.d.ts vendored
View file

@ -46,7 +46,7 @@ type PairingAuthorisation = {
primaryDevicePubKey: string;
secondaryDevicePubKey: string;
requestSignature: ArrayBuffer;
grantSignature: ArrayBuffer | null;
grantSignature?: ArrayBuffer;
};
type GuardNode = {
@ -153,25 +153,10 @@ export function removeAllContactSignedPreKeys(): Promise<void>;
export function createOrUpdatePairingAuthorisation(
data: PairingAuthorisation
): Promise<void>;
export function removePairingAuthorisationForSecondaryPubKey(
pubKey: string
): Promise<void>;
export function getGrantAuthorisationsForPrimaryPubKey(
export function getPairingAuthorisationsFor(
pubKey: string
): Promise<Array<PairingAuthorisation>>;
export function getGrantAuthorisationForSecondaryPubKey(
pubKey: string
): Promise<PairingAuthorisation | null>;
export function getAuthorisationForSecondaryPubKey(
pubKey: string
): Promise<PairingAuthorisation | null>;
export function getSecondaryDevicesFor(
primaryDevicePubKey: string
): Promise<Array<string>>;
export function getPrimaryDeviceFor(
secondaryDevicePubKey: string
): Promise<string | null>;
export function getPairedDevicesFor(pubKey: string): Promise<Array<string>>;
export function removePairingAuthorisationsFor(pubKey: string): Promise<void>;
// Guard Nodes
export function getGuardNodes(): Promise<GuardNode>;

View file

@ -89,13 +89,8 @@ module.exports = {
removeAllContactSignedPreKeys,
createOrUpdatePairingAuthorisation,
removePairingAuthorisationForSecondaryPubKey,
getGrantAuthorisationForSecondaryPubKey,
getAuthorisationForSecondaryPubKey,
getGrantAuthorisationsForPrimaryPubKey,
getSecondaryDevicesFor,
getPrimaryDeviceFor,
getPairedDevicesFor,
getPairingAuthorisationsFor,
removePairingAuthorisationsFor,
getGuardNodes,
updateGuardNodes,
@ -631,29 +626,20 @@ async function createOrUpdatePairingAuthorisation(data) {
});
}
async function removePairingAuthorisationForSecondaryPubKey(pubKey) {
if (!pubKey) {
return;
}
await channels.removePairingAuthorisationForSecondaryPubKey(pubKey);
async function getPairingAuthorisationsFor(pubKey) {
const authorisations = channels.getPairingAuthorisationsFor(pubKey);
return authorisations.map(authorisation => ({
...authorisation,
requestSignature: base64ToArrayBuffer(authorisation.requestSignature),
grantSignature: authorisation.grantSignature
? base64ToArrayBuffer(authorisation.grantSignature)
: undefined,
}));
}
async function getGrantAuthorisationForSecondaryPubKey(pubKey) {
return channels.getAuthorisationForSecondaryPubKey(pubKey, {
granted: true,
});
}
async function getGrantAuthorisationsForPrimaryPubKey(pubKey) {
return channels.getGrantAuthorisationsForPrimaryPubKey(pubKey);
}
function getAuthorisationForSecondaryPubKey(pubKey) {
return channels.getAuthorisationForSecondaryPubKey(pubKey);
}
function getSecondaryDevicesFor(primaryDevicePubKey) {
return channels.getSecondaryDevicesFor(primaryDevicePubKey);
async function removePairingAuthorisationsFor(pubKey) {
await channels.removePairingAuthorisationsFor(pubKey);
}
function getGuardNodes() {
@ -664,14 +650,6 @@ function updateGuardNodes(nodes) {
return channels.updateGuardNodes(nodes);
}
function getPrimaryDeviceFor(secondaryDevicePubKey) {
return channels.getPrimaryDeviceFor(secondaryDevicePubKey);
}
function getPairedDevicesFor(pubKey) {
return channels.getPairedDevicesFor(pubKey);
}
// Items
const ITEM_KEYS = {

16
js/modules/loki_file_server_api.d.ts vendored Normal file
View file

@ -0,0 +1,16 @@
interface FileServerPairingAuthorisation {
primaryDevicePubKey: string;
secondaryDevicePubKey: string;
requestSignature: string; // base64
grantSignature: string; // base64
}
interface DeviceMappingAnnotation {
isPrimary: string;
authorisations: Array<FileServerPairingAuthorisation>;
}
interface LokiFileServerInstance {
getUserDeviceMapping(pubKey: string): Promise<DeviceMappingAnnotation>;
clearOurDeviceMappingAnnotations(): Promise<void>;
}

View file

@ -1,6 +1,5 @@
/* global log, libloki, window */
/* global storage: false */
/* global Signal: false */
/* global log: false */
const LokiAppDotNetAPI = require('./loki_app_dot_net_api');
@ -220,16 +219,10 @@ class LokiHomeServerInstance extends LokiFileServerInstance {
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),
];
}
const authorisations = await window.libsession.Protocols.MultiDeviceProtocol.getPairingAuthorisations(
this.ourKey
);
return this._setOurDeviceMapping(authorisations, isPrimary);
}

View file

@ -1,4 +1,4 @@
/* global Whisper, i18n, textsecure, libloki, _ */
/* global Whisper, i18n, textsecure, _ */
// eslint-disable-next-line func-names
(function() {
@ -197,7 +197,7 @@
// exists in group, but hasn't yet synced with its other devices.
const getDevicesForRemoved = async () => {
const promises = notPresentInNew.map(member =>
libloki.storage.getPairedDevicesFor(member)
window.libsession.Protocols.MultiDeviceProtocol.getAllDevices(member)
);
const devices = _.flatten(await Promise.all(promises));

View file

@ -74,15 +74,6 @@
}
}
// Returns the primary device pubkey for this secondary device pubkey
// or the same pubkey if there is no other device
async function getPrimaryDevicePubkey(pubKey) {
const authorisation = await window.libloki.storage.getGrantAuthorisationForSecondaryPubKey(
pubKey
);
return authorisation ? authorisation.primaryDevicePubKey : pubKey;
}
async function sendSessionEstablishedMessage(pubKey) {
// This message shouldn't be routed through multi-device.
// It needs to go directly to the pubKey specified.
@ -330,7 +321,6 @@
createContactSyncProtoMessage,
createGroupSyncProtoMessage,
createOpenGroupsSyncProtoMessage,
getPrimaryDevicePubkey,
debug,
};
})();

View file

@ -1,13 +1,9 @@
/* global window, libsignal, textsecure, Signal,
lokiFileServerAPI, ConversationController */
/* global window, libsignal, textsecure */
// eslint-disable-next-line func-names
(function() {
window.libloki = window.libloki || {};
const timers = {};
const REFRESH_DELAY = 60 * 1000;
async function getPreKeyBundleForContact(pubKey) {
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
const identityKey = myKeyPair.pubKey;
@ -117,129 +113,6 @@
}
}
// fetches device mappings from server.
async function getPrimaryDeviceMapping(pubKey) {
if (typeof lokiFileServerAPI === 'undefined') {
// If this is not defined then we are initiating a pairing
return [];
}
const deviceMapping = await lokiFileServerAPI.getUserDeviceMapping(pubKey);
if (!deviceMapping) {
return [];
}
let authorisations = deviceMapping.authorisations || [];
if (deviceMapping.isPrimary === '0') {
const { primaryDevicePubKey } =
authorisations.find(
authorisation =>
authorisation && authorisation.secondaryDevicePubKey === pubKey
) || {};
if (primaryDevicePubKey) {
// do NOT call getprimaryDeviceMapping recursively
// in case both devices are out of sync and think they are
// each others' secondary pubkey.
const primaryDeviceMapping = await lokiFileServerAPI.getUserDeviceMapping(
primaryDevicePubKey
);
if (!primaryDeviceMapping) {
return [];
}
({ authorisations } = primaryDeviceMapping);
}
}
// filter out any invalid authorisations
return authorisations.filter(a => a && typeof a === 'object') || [];
}
// if the device is a secondary device,
// fetch the device mappings for its primary device
async function saveAllPairingAuthorisationsFor(pubKey) {
// Will be false if there is no timer
const cacheValid = timers[pubKey] > Date.now();
if (cacheValid) {
return;
}
timers[pubKey] = Date.now() + REFRESH_DELAY;
const authorisations = await getPrimaryDeviceMapping(pubKey);
await Promise.all(
authorisations.map(authorisation =>
savePairingAuthorisation(authorisation)
)
);
}
async function savePairingAuthorisation(authorisation) {
if (!authorisation) {
return;
}
// Ensure that we have a conversation for all the devices
const conversation = await ConversationController.getOrCreateAndWait(
authorisation.secondaryDevicePubKey,
'private'
);
await conversation.setSecondaryStatus(
true,
authorisation.primaryDevicePubKey
);
await window.Signal.Data.createOrUpdatePairingAuthorisation(authorisation);
}
function removePairingAuthorisationForSecondaryPubKey(pubKey) {
return window.Signal.Data.removePairingAuthorisationForSecondaryPubKey(
pubKey
);
}
// Transforms signatures from base64 to ArrayBuffer!
async function getGrantAuthorisationForSecondaryPubKey(secondaryPubKey) {
const conversation = ConversationController.get(secondaryPubKey);
if (!conversation || conversation.isPublic() || conversation.isRss()) {
return null;
}
await saveAllPairingAuthorisationsFor(secondaryPubKey);
const authorisation = await window.Signal.Data.getGrantAuthorisationForSecondaryPubKey(
secondaryPubKey
);
if (!authorisation) {
return null;
}
return {
...authorisation,
requestSignature: Signal.Crypto.base64ToArrayBuffer(
authorisation.requestSignature
),
grantSignature: Signal.Crypto.base64ToArrayBuffer(
authorisation.grantSignature
),
};
}
// Transforms signatures from base64 to ArrayBuffer!
async function getAuthorisationForSecondaryPubKey(secondaryPubKey) {
await saveAllPairingAuthorisationsFor(secondaryPubKey);
const authorisation = await window.Signal.Data.getAuthorisationForSecondaryPubKey(
secondaryPubKey
);
if (!authorisation) {
return null;
}
return {
...authorisation,
requestSignature: Signal.Crypto.base64ToArrayBuffer(
authorisation.requestSignature
),
grantSignature: authorisation.grantSignature
? Signal.Crypto.base64ToArrayBuffer(authorisation.grantSignature)
: null,
};
}
function getSecondaryDevicesFor(primaryDevicePubKey) {
return window.Signal.Data.getSecondaryDevicesFor(primaryDevicePubKey);
}
function getGuardNodes() {
return window.Signal.Data.getGuardNodes();
}
@ -248,31 +121,11 @@
return window.Signal.Data.updateGuardNodes(nodes);
}
async function getAllDevicePubKeysForPrimaryPubKey(primaryDevicePubKey) {
await saveAllPairingAuthorisationsFor(primaryDevicePubKey);
const secondaryPubKeys =
(await getSecondaryDevicesFor(primaryDevicePubKey)) || [];
return secondaryPubKeys.concat(primaryDevicePubKey);
}
function getPairedDevicesFor(pubkey) {
return window.Signal.Data.getPairedDevicesFor(pubkey);
}
window.libloki.storage = {
getPreKeyBundleForContact,
saveContactPreKeyBundle,
removeContactPreKeyBundle,
verifyFriendRequestAcceptPreKey,
savePairingAuthorisation,
saveAllPairingAuthorisationsFor,
removePairingAuthorisationForSecondaryPubKey,
getGrantAuthorisationForSecondaryPubKey,
getAuthorisationForSecondaryPubKey,
getPairedDevicesFor,
getAllDevicePubKeysForPrimaryPubKey,
getSecondaryDevicesFor,
getPrimaryDeviceMapping,
getGuardNodes,
updateGuardNodes,
};

View file

@ -3,6 +3,7 @@
textsecure,
libsignal,
libloki,
libsession,
lokiFileServerAPI,
mnemonic,
btoa,
@ -573,9 +574,12 @@
secondaryDevicePubKey,
libloki.crypto.PairingType.GRANT
);
const existingAuthorisation = await libloki.storage.getAuthorisationForSecondaryPubKey(
const authorisations = await libsession.Protocols.MultiDeviceProtocol.getPairingAuthorisations(
secondaryDevicePubKey
);
const existingAuthorisation = authorisations.some(
pairing => pairing.secondaryDevicePubKey === secondaryDevicePubKey
);
if (!existingAuthorisation) {
throw new Error(
'authoriseSecondaryDevice: request signature missing from database!'
@ -588,8 +592,11 @@
requestSignature,
grantSignature,
};
// Update authorisation in database with the new grant signature
await libloki.storage.savePairingAuthorisation(authorisation);
await libsession.Protocols.MultiDeviceProtocol.savePairingAuthorisation(
authorisation
);
// Try to upload to the file server and then send a message
try {
@ -604,7 +611,7 @@
e && e.stack ? e.stack : e
);
// File server upload failed or message sending failed, we should rollback changes
await libloki.storage.removePairingAuthorisationForSecondaryPubKey(
await libsession.Protocols.MultiDeviceProtocol.removePairingAuthorisations(
secondaryDevicePubKey
);
await lokiFileServerAPI.updateOurDeviceMapping();

View file

@ -968,7 +968,9 @@ MessageReceiver.prototype.extend({
if (valid) {
// Pairing dialog is open and is listening
if (Whisper.events.isListenedTo('devicePairingRequestReceived')) {
await window.libloki.storage.savePairingAuthorisation(pairingRequest);
await window.libsession.Protocols.MultiDeviceProtocol.savePairingAuthorisation(
pairingRequest
);
Whisper.events.trigger(
'devicePairingRequestReceived',
pairingRequest.secondaryDevicePubKey
@ -1014,7 +1016,9 @@ MessageReceiver.prototype.extend({
window.storage.remove('secondaryDeviceStatus');
window.storage.put('isSecondaryDevice', true);
window.storage.put('primaryDevicePubKey', primaryDevicePubKey);
await libloki.storage.savePairingAuthorisation(pairingAuthorisation);
await window.libsession.Protocols.MultiDeviceProtocol.savePairingAuthorisation(
pairingAuthorisation
);
const primaryConversation = await ConversationController.getOrCreateAndWait(
primaryDevicePubKey,
'private'
@ -1270,16 +1274,12 @@ MessageReceiver.prototype.extend({
async handleSyncMessage(envelope, syncMessage) {
// We should only accept sync messages from our devices
const ourNumber = textsecure.storage.user.getNumber();
const ourPrimaryNumber = window.storage.get('primaryDevicePubKey');
const ourOtherDevices = await libloki.storage.getAllDevicePubKeysForPrimaryPubKey(
ourPrimaryNumber
const ourDevices = await libsession.Protocols.MultiDeviceProtocol.getAllDevices(
ourNumber
);
const validSyncSender = ourDevices.some(
device => device.key === envelope.source
);
const ourDevices = new Set([
ourNumber,
ourPrimaryNumber,
...ourOtherDevices,
]);
const validSyncSender = ourDevices.has(envelope.source);
if (!validSyncSender) {
throw new Error(
"Received sync message from a device we aren't paired with"

View file

@ -431,13 +431,15 @@ OutgoingMessage.prototype = {
const ourPubKey = textsecure.storage.user.getNumber();
const ourPrimaryPubkey = window.storage.get('primaryDevicePubKey');
const secondaryPubKeys =
(await window.libloki.storage.getSecondaryDevicesFor(ourPubKey)) || [];
(await window.libsession.Protocols.MultiDeviceProtocol.getSecondaryDevices(
ourPubKey
)) || [];
let aliasedPubkey = devicePubKey;
if (devicePubKey === ourPubKey) {
aliasedPubkey = 'OUR_PUBKEY'; // should not happen
} else if (devicePubKey === ourPrimaryPubkey) {
aliasedPubkey = 'OUR_PRIMARY_PUBKEY';
} else if (secondaryPubKeys.includes(devicePubKey)) {
} else if (secondaryPubKeys.some(device => device.key === devicePubKey)) {
aliasedPubkey = 'OUR SECONDARY PUBKEY';
}
libloki.api.debug.logSessionMessageSending(

View file

@ -537,12 +537,12 @@ MessageSender.prototype = {
window.storage.get('primaryDevicePubKey') ||
textsecure.storage.user.getNumber();
const allOurDevices = (
await libloki.storage.getAllDevicePubKeysForPrimaryPubKey(
await window.libsession.Protocols.MultiDeviceProtocol.getAllDevices(
primaryDeviceKey
)
)
// Don't send to ourselves
.filter(pubKey => pubKey !== textsecure.storage.user.getNumber());
.filter(pubKey => pubKey.key !== textsecure.storage.user.getNumber());
if (allOurDevices.length === 0) {
return null;
}

View file

@ -7,6 +7,9 @@ import {
SessionButtonColor,
SessionButtonType,
} from '../SessionButton';
import { UserUtil } from '../../../util';
import { MultiDeviceProtocol } from '../../../session/protocols';
import { PubKey } from '../../../session/types';
export enum SessionSettingCategory {
Appearance = 'appearance',
@ -80,10 +83,10 @@ export class SettingsView extends React.Component<SettingsViewProps, State> {
window.Whisper.events.on('refreshLinkedDeviceList', async () => {
setTimeout(() => {
this.refreshLinkedDevice();
void this.refreshLinkedDevice();
}, 1000);
});
this.refreshLinkedDevice();
void this.refreshLinkedDevice();
}
public componentWillUnmount() {
@ -644,16 +647,14 @@ export class SettingsView extends React.Component<SettingsViewProps, State> {
}
}
private refreshLinkedDevice() {
const ourPubKey = window.textsecure.storage.user.getNumber();
private async refreshLinkedDevice() {
const ourPubKey = await UserUtil.getCurrentDevicePubKey();
if (ourPubKey) {
const pubKey = new PubKey(ourPubKey);
const devices = await MultiDeviceProtocol.getSecondaryDevices(pubKey);
window.libloki.storage
.getSecondaryDevicesFor(ourPubKey)
.then((pubKeys: any) => {
this.setState({
linkedPubKeys: pubKeys,
});
});
this.setState({ linkedPubKeys: devices.map(d => d.key) });
}
}
private async onKeyUp(event: any) {

View file

@ -3,6 +3,9 @@ import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon';
import { SessionSettingCategory, SettingsViewProps } from './SessionSettings';
import { SessionButton } from '../SessionButton';
import { UserUtil } from '../../../util';
import { PubKey } from '../../../session/types';
import { MultiDeviceProtocol } from '../../../session/protocols';
interface Props extends SettingsViewProps {
// showLinkDeviceButton is used to completely hide the button while the settings password lock is displayed
@ -37,22 +40,20 @@ export class SettingsHeader extends React.Component<Props, any> {
public componentDidMount() {
if (!this.props.isSecondaryDevice) {
window.Whisper.events.on('refreshLinkedDeviceList', async () => {
this.refreshLinkedDevice();
void this.refreshLinkedDevice();
});
this.refreshLinkedDevice();
void this.refreshLinkedDevice();
}
}
public refreshLinkedDevice() {
const ourPubKey = window.textsecure.storage.user.getNumber();
public async refreshLinkedDevice() {
const ourPubKey = await UserUtil.getCurrentDevicePubKey();
if (ourPubKey) {
const pubKey = new PubKey(ourPubKey);
const devices = await MultiDeviceProtocol.getSecondaryDevices(pubKey);
window.libloki.storage
.getSecondaryDevicesFor(ourPubKey)
.then((pubKeys: any) => {
this.setState({
disableLinkDeviceButton: pubKeys && pubKeys.length > 0,
});
});
this.setState({ disableLinkDeviceButton: devices.length > 0 });
}
}
public componentWillUnmount() {

View file

@ -15,6 +15,8 @@ import { SignalService } from './../protobuf';
import { removeFromCache } from './cache';
import { toNumber } from 'lodash';
import { DataMessage } from '../session/messages/outgoing';
import { MultiDeviceProtocol } from '../session/protocols';
import { PubKey } from '../session/types';
export { handleEndSession, handleMediumGroupUpdate };
@ -265,7 +267,12 @@ async function handleSecondaryDeviceFriendRequest(pubKey: string) {
if (!c || !(await c.isFriendWithAnyDevice())) {
return false;
}
await window.libloki.storage.savePairingAuthorisation(authorisation);
await MultiDeviceProtocol.savePairingAuthorisation({
primaryDevicePubKey: authorisation.primaryDevicePubKey,
secondaryDevicePubKey: authorisation.secondaryDevicePubKey,
requestSignature: Buffer.from(authorisation.requestSignature).buffer,
grantSignature: Buffer.from(authorisation.grantSignature).buffer,
});
return true;
}
@ -601,14 +608,11 @@ export async function handleDataMessage(
const source = envelope.senderIdentity || senderPubKey;
const isOwnDevice = async (pubkey: string) => {
const primaryDevice = window.storage.get('primaryDevicePubKey');
const secondaryDevices = await window.libloki.storage.getPairedDevicesFor(
primaryDevice
);
const isOwnDevice = async (device: string) => {
const pubKey = new PubKey(device);
const allDevices = await MultiDeviceProtocol.getAllDevices(pubKey);
const allDevices = [primaryDevice, ...secondaryDevices];
return allDevices.includes(pubkey);
return allDevices.some(d => PubKey.isEqual(d, pubKey));
};
const ownDevice = await isOwnDevice(source);
@ -773,13 +777,7 @@ export async function handleMessageEvent(event: any): Promise<void> {
// - group.id if it is a group message
let conversationId = id;
const authorisation = await window.libloki.storage.getGrantAuthorisationForSecondaryPubKey(
source
);
const primarySource =
(authorisation && authorisation.primaryDevicePubKey) || source;
const primarySource = await MultiDeviceProtocol.getPrimaryDevice(source);
if (isGroupMessage) {
/* handle one part of the group logic here:
handle requesting info of a new group,
@ -789,7 +787,7 @@ export async function handleMessageEvent(event: any): Promise<void> {
const shouldReturn = await preprocessGroupMessage(
source,
message.group,
primarySource
primarySource.key
);
// handleGroupMessage() can process fully a message in some cases
@ -800,9 +798,9 @@ export async function handleMessageEvent(event: any): Promise<void> {
}
}
if (source !== ourNumber && authorisation) {
if (source !== ourNumber) {
// Ignore auth from our devices
conversationId = authorisation.primaryDevicePubKey;
conversationId = primarySource.key;
}
// the conversation with the primary device of that source (can be the same as conversationOrigin)

View file

@ -1,8 +1,9 @@
import * as Messages from './messages';
import * as Protocols from './protocols';
import * as Types from './types';
// TODO: Do we export class instances here?
// E.g
// export const messageQueue = new MessageQueue()
export { Messages, Protocols };
export { Messages, Protocols, Types };

View file

@ -1,6 +1,222 @@
// TODO: Populate this with multi device specific code, e.g getting linked devices for a user etc...
// We need to deprecate the multi device code we have in js and slowly transition to this file
import _ from 'lodash';
import {
createOrUpdatePairingAuthorisation,
getPairingAuthorisationsFor,
PairingAuthorisation,
removePairingAuthorisationsFor,
} from '../../../js/modules/data';
import { PrimaryPubKey, PubKey, SecondaryPubKey } from '../types';
import { UserUtil } from '../../util';
export function implementStuffHere() {
throw new Error("Don't call me :(");
/*
The reason we're exporing a class here instead of just exporting the functions directly is for the sake of testing.
We might want to stub out specific functions inside the multi device protocol itself but when exporting functions directly then it's not possible without weird hacks.
*/
// tslint:disable-next-line: no-unnecessary-class
export class MultiDeviceProtocol {
public static refreshDelay: number = 5 * 60 * 1000; // 5 minutes
private static lastFetch: { [device: string]: number } = {};
/**
* Fetch pairing authorisations from the file server if needed and store it in the database.
*
* This will fetch authorisations if:
* - It is not one of our device
* - The time since last fetch is more than refresh delay
*/
public static async fetchPairingAuthorisationsIfNeeded(
device: PubKey
): Promise<void> {
// This return here stops an infinite loop when we get all our other devices
const ourKey = await UserUtil.getCurrentDevicePubKey();
if (!ourKey || device.key === ourKey) {
return;
}
// We always prefer our local pairing over the one on the server
const ourDevices = await this.getAllDevices(ourKey);
if (ourDevices.some(d => d.key === device.key)) {
return;
}
// Only fetch if we hit the refresh delay
const lastFetchTime = this.lastFetch[device.key];
if (lastFetchTime && lastFetchTime + this.refreshDelay < Date.now()) {
return;
}
this.lastFetch[device.key] = Date.now();
try {
const authorisations = await this.fetchPairingAuthorisations(device);
// TODO: validate?
await Promise.all(authorisations.map(this.savePairingAuthorisation));
} catch (e) {
// Something went wrong, let it re-try another time
this.lastFetch[device.key] = lastFetchTime;
}
}
/**
* Reset the pairing fetched cache.
*
* This will make it so the next call to `fetchPairingAuthorisationsIfNeeded` will fetch mappings from the server.
*/
public static resetFetchCache() {
this.lastFetch = {};
}
/**
* Fetch pairing authorisations for the given device from the file server.
* This function will not save the authorisations to the database.
*
* @param device The device to fetch the authorisation for.
*/
public static async fetchPairingAuthorisations(
device: PubKey
): Promise<Array<PairingAuthorisation>> {
if (!window.lokiFileServerAPI) {
throw new Error('lokiFileServerAPI is not initialised.');
}
const mapping = await window.lokiFileServerAPI.getUserDeviceMapping(
device.key
);
// TODO: Filter out invalid authorisations
return mapping.authorisations.map(
({
primaryDevicePubKey,
secondaryDevicePubKey,
requestSignature,
grantSignature,
}) => ({
primaryDevicePubKey,
secondaryDevicePubKey,
requestSignature: Buffer.from(requestSignature, 'base64').buffer,
grantSignature: Buffer.from(grantSignature, 'base64').buffer,
})
);
}
/**
* Save pairing authorisation to the database.
* @param authorisation The pairing authorisation.
*/
public static async savePairingAuthorisation(
authorisation: PairingAuthorisation
): Promise<void> {
return createOrUpdatePairingAuthorisation(authorisation);
}
/**
* Get pairing authorisations for a given device.
* @param device The device to get pairing authorisations for.
*/
public static async getPairingAuthorisations(
device: PubKey | string
): Promise<Array<PairingAuthorisation>> {
const pubKey = typeof device === 'string' ? new PubKey(device) : device;
await this.fetchPairingAuthorisationsIfNeeded(pubKey);
return getPairingAuthorisationsFor(pubKey.key);
}
/**
* Remove all pairing authorisations for a given device.
* @param device The device to remove authorisation for.
*/
public static async removePairingAuthorisations(
device: PubKey | string
): Promise<void> {
const pubKey = typeof device === 'string' ? new PubKey(device) : device;
return removePairingAuthorisationsFor(pubKey.key);
}
/**
* Get all devices linked to a user.
*
* @param user The user to get all the devices from.
*/
public static async getAllDevices(
user: PubKey | string
): Promise<Array<PubKey>> {
const pubKey = typeof user === 'string' ? new PubKey(user) : user;
const authorisations = await this.getPairingAuthorisations(pubKey);
const devices = _.flatMap(
authorisations,
({ primaryDevicePubKey, secondaryDevicePubKey }) => [
primaryDevicePubKey,
secondaryDevicePubKey,
]
);
return _.uniq(devices).map(p => new PubKey(p));
}
/**
* Get the primary device linked to a user.
*
* @param user The user to get primary device for.
*/
public static async getPrimaryDevice(
user: PubKey | string
): Promise<PrimaryPubKey> {
const pubKey = typeof user === 'string' ? new PubKey(user) : user;
const authorisations = await this.getPairingAuthorisations(pubKey);
if (authorisations.length === 0) {
return pubKey;
}
const primary = PrimaryPubKey.from(authorisations[0].primaryDevicePubKey);
if (!primary) {
throw new Error(`Primary user public key is invalid for ${pubKey.key}.`);
}
return primary;
}
/**
* Get all the secondary devices linked to a user.
*
* @param user The user to get the devices from.
*/
public static async getSecondaryDevices(
user: PubKey | string
): Promise<Array<SecondaryPubKey>> {
const primary = await this.getPrimaryDevice(user);
const authorisations = await this.getPairingAuthorisations(primary);
return authorisations
.map(a => a.secondaryDevicePubKey)
.map(pubKey => new SecondaryPubKey(pubKey));
}
/**
* Get all devices linked to the current user.
*/
public static async getOurDevices(): Promise<Array<PubKey>> {
const ourPubKey = await UserUtil.getCurrentDevicePubKey();
if (!ourPubKey) {
throw new Error('Public key not set.');
}
return this.getAllDevices(ourPubKey);
}
/**
* Check if the given device is one of our own.
* @param device The device to check.
*/
public static async isOurDevice(device: PubKey | string): Promise<boolean> {
const pubKey = typeof device === 'string' ? new PubKey(device) : device;
try {
const ourDevices = await this.getOurDevices();
return ourDevices.some(d => PubKey.isEqual(d, pubKey));
} catch (e) {
return false;
}
}
}

View file

@ -1,4 +1,4 @@
import { SessionProtocol } from './SessionProtocol';
import * as MultiDeviceProtocol from './MultiDeviceProtocol';
export * from './MultiDeviceProtocol';
export { SessionProtocol, MultiDeviceProtocol };
export { SessionProtocol };

View file

@ -1,5 +1,3 @@
import { getPairedDevicesFor } from '../../../js/modules/data';
import { EventEmitter } from 'events';
import {
MessageQueueInterface,
@ -20,7 +18,7 @@ import {
} from '../utils';
import { PubKey } from '../types';
import { MessageSender } from '.';
import { SessionProtocol } from '../protocols';
import { MultiDeviceProtocol, SessionProtocol } from '../protocols';
import { UserUtil } from '../../util';
export class MessageQueue implements MessageQueueInterface {
@ -35,8 +33,7 @@ export class MessageQueue implements MessageQueueInterface {
}
public async sendUsingMultiDevice(user: PubKey, message: ContentMessage) {
const userLinked = await getPairedDevicesFor(user.key);
const userDevices = userLinked.map(d => new PubKey(d));
const userDevices = await MultiDeviceProtocol.getAllDevices(user.key);
await this.sendMessageToDevices(userDevices, message);
}
@ -56,11 +53,10 @@ export class MessageQueue implements MessageQueueInterface {
const currentDevice = await UserUtil.getCurrentDevicePubKey();
if (currentDevice) {
const otherDevices = await getPairedDevicesFor(currentDevice);
const ourDevices = [currentDevice, ...otherDevices].map(
device => new PubKey(device)
const ourDevices = await MultiDeviceProtocol.getAllDevices(
currentDevice
);
await this.sendSyncMessage(message, ourDevices);
// Remove our devices from currentDevices

View file

@ -31,3 +31,6 @@ export class PubKey {
return key.key === comparator.key;
}
}
export class PrimaryPubKey extends PubKey {}
export class SecondaryPubKey extends PubKey {}

View file

@ -1,11 +1,8 @@
import * as _ from 'lodash';
import * as UserUtils from '../../util/user';
import {
getAllConversations,
getPrimaryDeviceFor,
} from '../../../js/modules/data';
import { getAllConversations } from '../../../js/modules/data';
import { ContentMessage, SyncMessage } from '../messages/outgoing';
import { MultiDeviceProtocol } from '../protocols';
export function from(message: ContentMessage): SyncMessage | undefined {
// const { timestamp, identifier } = message;
@ -29,7 +26,7 @@ export async function getSyncContacts(): Promise<Array<any> | undefined> {
return [];
}
const primaryDevice = await getPrimaryDeviceFor(thisDevice);
const primaryDevice = await MultiDeviceProtocol.getPrimaryDevice(thisDevice);
const conversations = await getAllConversations({
ConversationCollection: window.Whisper.ConversationCollection,
});
@ -61,7 +58,7 @@ export async function getSyncContacts(): Promise<Array<any> | undefined> {
const secondaryContacts = (await Promise.all(seondaryContactsPromise))
// Filter out our primary key if it was added here
.filter(c => c.id !== primaryDevice);
.filter(c => c.id !== primaryDevice.key);
// Return unique contacts
return _.uniqBy(

View file

@ -5,11 +5,7 @@ import { AdvancedSearchOptions, SearchOptions } from '../../types/Search';
import { trigger } from '../../shims/events';
import { getMessageModel } from '../../shims/Whisper';
import { cleanSearchTerm } from '../../util/cleanSearchTerm';
import {
getPrimaryDeviceFor,
searchConversations,
searchMessages,
} from '../../../js/modules/data';
import { searchConversations, searchMessages } from '../../../js/modules/data';
import { makeLookup } from '../../util/makeLookup';
import {
@ -19,6 +15,8 @@ import {
RemoveAllConversationsActionType,
SelectedConversationChangedActionType,
} from './conversations';
import { MultiDeviceProtocol } from '../../session/protocols';
import { PubKey } from '../../session/types';
// State
@ -283,15 +281,13 @@ async function queryConversationsAndContacts(
query
);
const ourPrimaryDevice = isSecondaryDevice
? await getPrimaryDeviceFor(ourNumber)
: ourNumber;
const ourPrimaryDevice = await MultiDeviceProtocol.getPrimaryDevice(
ourNumber
);
const resultPrimaryDevices: Array<string | null> = await Promise.all(
const resultPrimaryDevices = await Promise.all(
searchResults.map(async conversation =>
conversation.id === ourPrimaryDevice
? Promise.resolve(ourPrimaryDevice)
: getPrimaryDeviceFor(conversation.id)
MultiDeviceProtocol.getPrimaryDevice(conversation.id)
)
);
@ -304,10 +300,13 @@ async function queryConversationsAndContacts(
const primaryDevice = resultPrimaryDevices[i];
if (primaryDevice) {
if (isSecondaryDevice && primaryDevice === ourPrimaryDevice) {
if (
isSecondaryDevice &&
PubKey.isEqual(primaryDevice, ourPrimaryDevice)
) {
conversations.push(ourNumber);
} else {
conversations.push(primaryDevice);
conversations.push(primaryDevice.key);
}
} else if (conversation.type === 'direct') {
contacts.push(conversation.id);

2
ts/window.d.ts vendored
View file

@ -46,7 +46,7 @@ declare global {
libsignal: LibsignalProtocol;
log: any;
lokiFeatureFlags: any;
lokiFileServerAPI: any;
lokiFileServerAPI: LokiFileServerInstance;
lokiMessageAPI: LokiMessageAPI;
lokiPublicChatAPI: LokiPublicChatFactoryAPI;
mnemonic: any;