diff --git a/js/modules/loki_file_server_api.d.ts b/js/modules/loki_file_server_api.d.ts new file mode 100644 index 000000000..a1c1b0a71 --- /dev/null +++ b/js/modules/loki_file_server_api.d.ts @@ -0,0 +1,13 @@ +interface DeviceMappingAnnotation { + isPrimary: boolean; + authorisations: Array<{ + primaryDevicePubKey: string; + secondaryDevicePubKey: string; + requestSignature: string; // base64 + grantSignature: string; // base64 + }>; +} + +interface LokiFileServerInstance { + getUserDeviceMapping(pubKey: string): Promise; +} diff --git a/ts/session/protocols/MultiDeviceProtocol.ts b/ts/session/protocols/MultiDeviceProtocol.ts index 737ea13b0..9b41cb945 100644 --- a/ts/session/protocols/MultiDeviceProtocol.ts +++ b/ts/session/protocols/MultiDeviceProtocol.ts @@ -6,8 +6,8 @@ import { removePairingAuthorisationsFor, } from '../../../js/modules/data'; import { PrimaryPubKey, PubKey, SecondaryPubKey } from '../types'; - -// TODO: We should fetch mappings when we can and only fetch them once every 5 minutes or something +import { UserUtil } from '../../util'; +import { lokiFileServerAPI } from '../../window'; /* The reason we're exporing a class here instead of just exporting the functions directly is for the sake of testing. @@ -15,6 +15,88 @@ import { PrimaryPubKey, PubKey, SecondaryPubKey } from '../types'; */ // tslint:disable-next-line: no-unnecessary-class export class MultiDeviceProtocol { + public static refreshDelay: number = 5 * 1000 * 1000; // 5 minutes + private static lastFetch: { [device: string]: number } = {}; + + /** + * Fetch pairing authorisations from the file server if needed. + * This shouldn't be called outside of the MultiDeviceProtocol file, it is public so it can be stubbed in tests. + * + * 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 { + // 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; + } + } + + /** + * This function shouldn't be called outside of tests!! + */ + 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> { + if (!lokiFileServerAPI) { + throw new Error('lokiFileServerAPI is not initialised.'); + } + + const mapping = await 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. @@ -33,6 +115,7 @@ export class MultiDeviceProtocol { device: PubKey | string ): Promise> { const pubKey = typeof device === 'string' ? new PubKey(device) : device; + await this._fetchPairingAuthorisationsIfNeeded(pubKey); return getPairingAuthorisationsFor(pubKey.key); } diff --git a/ts/window/index.ts b/ts/window/index.ts index 4688cde7b..6533ff1c5 100644 --- a/ts/window/index.ts +++ b/ts/window/index.ts @@ -76,6 +76,7 @@ interface WindowInterface extends Window { lokiMessageAPI: LokiMessageAPI; lokiPublicChatAPI: LokiPublicChatFactoryAPI; + lokiFileServerAPI: LokiFileServerInstance; } // In the case for tests @@ -140,3 +141,4 @@ export const textsecure = window.textsecure; export const lokiMessageAPI = window.lokiMessageAPI; export const lokiPublicChatAPI = window.lokiPublicChatAPI; +export const lokiFileServerAPI = window.lokiFileServerAPI; diff --git a/tslint.json b/tslint.json index 628e59a93..6059de7c7 100644 --- a/tslint.json +++ b/tslint.json @@ -93,10 +93,10 @@ true, { "function-regex": "^_?[a-z][\\w\\d]+$", - "method-regex": "^[a-z][\\w\\d]+$", + "method-regex": "^_?[a-z][\\w\\d]+$", "private-method-regex": "^[a-z][\\w\\d]+$", "protected-method-regex": "^[a-z][\\w\\d]+$", - "static-method-regex": "^[a-zA-Z][\\w\\d]+$" + "static-method-regex": "^_?[a-zA-Z][\\w\\d]+$" } ],