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.
session-desktop/ts/session/protocols/MultiDeviceProtocol.ts

250 lines
7.4 KiB
TypeScript

import _ from 'lodash';
import {
createOrUpdatePairingAuthorisation,
getPairingAuthorisationsFor,
PairingAuthorisation,
removePairingAuthorisationsFor,
} from '../../../js/modules/data';
import { PrimaryPubKey, PubKey, SecondaryPubKey } from '../types';
import { UserUtil } from '../../util';
import { StringUtils } from '../utils';
/*
The reason we're exporing a class here instead of just exporting the functions directly is for the sake of testing.
5 years ago
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 {
5 years ago
public static refreshDelay: number = 5 * 60 * 1000; // 5 minutes
private static lastFetch: { [device: string]: number } = {};
/**
5 years ago
* 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
*/
5 years ago
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 isOurDevice = await this.isOurDevice(device);
if (isOurDevice) {
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);
await Promise.all(authorisations.map(this.savePairingAuthorisation));
} catch (e) {
// Something went wrong, let it re-try another time
this.lastFetch[device.key] = lastFetchTime;
}
}
/**
5 years ago
* Reset the pairing fetched cache.
*
* This will make it so the next call to `fetchPairingAuthorisationsIfNeeded` will fetch mappings from the server.
*/
5 years ago
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>> {
5 years ago
if (!window.lokiFileServerAPI) {
throw new Error('lokiFileServerAPI is not initialised.');
}
5 years ago
const mapping = await window.lokiFileServerAPI.getUserDeviceMapping(
device.key
);
if (!mapping || !mapping.authorisations) {
return [];
}
try {
const authorisations = mapping.authorisations.map(
({
primaryDevicePubKey,
secondaryDevicePubKey,
requestSignature,
grantSignature,
}) => ({
primaryDevicePubKey,
secondaryDevicePubKey,
requestSignature: StringUtils.encode(requestSignature, 'base64'),
grantSignature: StringUtils.encode(grantSignature, 'base64'),
})
);
const validAuthorisations = await Promise.all(
authorisations.map(async authoritsation => {
const valid = await window.libloki.crypto.verifyAuthorisation(
authoritsation
);
return valid ? authoritsation : undefined;
})
);
return validAuthorisations.filter(a => !!a) as Array<
PairingAuthorisation
>;
} catch (e) {
console.warn(
`MultiDeviceProtocol::fetchPairingAuthorisation: Failed to map authorisations for ${device.key}.`,
e
);
return [];
}
}
/**
* 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;
5 years ago
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);
if (authorisations.length === 0) {
5 years ago
return [pubKey];
}
const devices = _.flatMap(
authorisations,
({ primaryDevicePubKey, secondaryDevicePubKey }) => [
primaryDevicePubKey,
secondaryDevicePubKey,
]
);
5 years ago
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 => d.isEqual(pubKey));
} catch (e) {
return false;
}
}
}