Merge pull request #1176 from Mikunj/multi-device-protocol
Multidevice Protocol Refactorpull/1180/head
commit
ce868456c2
@ -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>;
|
||||||
|
}
|
@ -1,8 +1,9 @@
|
|||||||
import * as Messages from './messages';
|
import * as Messages from './messages';
|
||||||
import * as Protocols from './protocols';
|
import * as Protocols from './protocols';
|
||||||
|
import * as Types from './types';
|
||||||
|
|
||||||
// TODO: Do we export class instances here?
|
// TODO: Do we export class instances here?
|
||||||
// E.g
|
// E.g
|
||||||
// export const messageQueue = new MessageQueue()
|
// export const messageQueue = new MessageQueue()
|
||||||
|
|
||||||
export { Messages, Protocols };
|
export { Messages, Protocols, Types };
|
||||||
|
@ -1,6 +1,222 @@
|
|||||||
// TODO: Populate this with multi device specific code, e.g getting linked devices for a user etc...
|
import _ from 'lodash';
|
||||||
// We need to deprecate the multi device code we have in js and slowly transition to this file
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { SessionProtocol } from './SessionProtocol';
|
import { SessionProtocol } from './SessionProtocol';
|
||||||
import * as MultiDeviceProtocol from './MultiDeviceProtocol';
|
export * from './MultiDeviceProtocol';
|
||||||
|
|
||||||
export { SessionProtocol, MultiDeviceProtocol };
|
export { SessionProtocol };
|
||||||
|
Loading…
Reference in New Issue