From e0f27ba712a0cbd07622a6ce6e05cfb7265d3d74 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 22 May 2020 10:25:30 +1000 Subject: [PATCH] basic classes for message sending --- ts/session/crypto/MessageEncrypter.ts | 39 ++++++++++++++ ts/session/crypto/index.ts | 3 ++ ts/session/index.ts | 8 +++ ts/session/protocols/MultiDeviceProtocol.ts | 6 +++ ts/session/protocols/SessionProtocol.ts | 57 +++++++++++++++++++++ ts/session/protocols/index.ts | 4 ++ ts/session/sending/MessageQueue.ts | 54 +++++++++++++++++++ ts/session/sending/MessageQueueInterface.ts | 13 +++++ ts/session/sending/MessageSender.ts | 14 +++++ ts/session/sending/PendingMessageCache.ts | 36 +++++++++++++ ts/session/sending/index.ts | 5 ++ ts/session/types/EncryptionType.ts | 5 ++ ts/session/types/RawMessage.ts | 12 +++++ ts/session/utils/JobQueue.ts | 40 +++++++++++++++ 14 files changed, 296 insertions(+) create mode 100644 ts/session/crypto/MessageEncrypter.ts create mode 100644 ts/session/crypto/index.ts create mode 100644 ts/session/index.ts create mode 100644 ts/session/protocols/MultiDeviceProtocol.ts create mode 100644 ts/session/protocols/SessionProtocol.ts create mode 100644 ts/session/protocols/index.ts create mode 100644 ts/session/sending/MessageQueue.ts create mode 100644 ts/session/sending/MessageQueueInterface.ts create mode 100644 ts/session/sending/MessageSender.ts create mode 100644 ts/session/sending/PendingMessageCache.ts create mode 100644 ts/session/sending/index.ts create mode 100644 ts/session/types/EncryptionType.ts create mode 100644 ts/session/types/RawMessage.ts create mode 100644 ts/session/utils/JobQueue.ts diff --git a/ts/session/crypto/MessageEncrypter.ts b/ts/session/crypto/MessageEncrypter.ts new file mode 100644 index 000000000..8b2457961 --- /dev/null +++ b/ts/session/crypto/MessageEncrypter.ts @@ -0,0 +1,39 @@ +import { EncryptionType } from '../types/EncryptionType'; +import { SignalService } from '../../protobuf'; + +function padPlainTextBuffer(messageBuffer: Uint8Array): Uint8Array { + const plaintext = new Uint8Array( + getPaddedMessageLength(messageBuffer.byteLength + 1) - 1 + ); + plaintext.set(new Uint8Array(messageBuffer)); + plaintext[messageBuffer.byteLength] = 0x80; + + return plaintext; +} + +function getPaddedMessageLength(originalLength: number): number { + const messageLengthWithTerminator = originalLength + 1; + let messagePartCount = Math.floor(messageLengthWithTerminator / 160); + + if (messageLengthWithTerminator % 160 !== 0) { + messagePartCount += 1; + } + + return messagePartCount * 160; +} + +export function encrypt( + device: string, + plainTextBuffer: Uint8Array, + encryptionType: EncryptionType +): { + envelopeType: SignalService.Envelope.Type; + cipherText: Uint8Array; +} { + const plainText = padPlainTextBuffer(plainTextBuffer); + // TODO: Do encryption here? + return { + envelopeType: SignalService.Envelope.Type.CIPHERTEXT, + cipherText: new Uint8Array(), + }; +} diff --git a/ts/session/crypto/index.ts b/ts/session/crypto/index.ts new file mode 100644 index 000000000..02d1b8904 --- /dev/null +++ b/ts/session/crypto/index.ts @@ -0,0 +1,3 @@ +import * as MessageEncrypter from './MessageEncrypter'; + +export { MessageEncrypter }; diff --git a/ts/session/index.ts b/ts/session/index.ts new file mode 100644 index 000000000..4aeab7901 --- /dev/null +++ b/ts/session/index.ts @@ -0,0 +1,8 @@ +import * as Messages from './messages'; +import * as Protocols from './protocols'; + +// TODO: Do we export class instances here? +// E.g +// export const messageQueue = new MessageQueue() + +export { Messages, Protocols }; diff --git a/ts/session/protocols/MultiDeviceProtocol.ts b/ts/session/protocols/MultiDeviceProtocol.ts new file mode 100644 index 000000000..b144c20cf --- /dev/null +++ b/ts/session/protocols/MultiDeviceProtocol.ts @@ -0,0 +1,6 @@ +// 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 + +export function implementStuffHere() { + throw new Error("Don't call me :("); +} diff --git a/ts/session/protocols/SessionProtocol.ts b/ts/session/protocols/SessionProtocol.ts new file mode 100644 index 000000000..3f9cf2dc8 --- /dev/null +++ b/ts/session/protocols/SessionProtocol.ts @@ -0,0 +1,57 @@ +// TODO: Need to flesh out these functions +// Structure of this can be changed for example sticking this all in a class +// The reason i haven't done it is to avoid having instances of the protocol, rather you should be able to call the functions directly + +import { OutgoingContentMessage } from '../messages/outgoing'; + +export function hasSession(device: string): boolean { + return false; // TODO: Implement +} + +export function hasSentSessionRequest(device: string): boolean { + // TODO: need a way to keep track of if we've sent a session request + // My idea was to use the timestamp of when it was sent but there might be another better approach + return false; +} + +export async function sendSessionRequestIfNeeded( + device: string +): Promise { + if (hasSession(device) || hasSentSessionRequest(device)) { + return Promise.resolve(); + } + + // TODO: Call sendSessionRequest with SessionReset + return Promise.reject(new Error('Need to implement this function')); +} + +// TODO: Replace OutgoingContentMessage with SessionReset +export async function sendSessionRequest( + message: OutgoingContentMessage +): Promise { + // TODO: Optimistically store timestamp of when session request was sent + // TODO: Send out the request via MessageSender + // TODO: On failure, unset the timestamp + return Promise.resolve(); +} + +export async function sessionEstablished(device: string) { + // TODO: this is called when we receive an encrypted message from the other user + // Maybe it should be renamed to something else + // TODO: This should make `hasSentSessionRequest` return `false` +} + +export async function shouldProcessSessionRequest( + device: string, + messageTimestamp: number +): boolean { + // TODO: Need to do the following here + // messageTimestamp > session request sent timestamp && messageTimestamp > session request processed timestamp + return false; +} + +export async function sessionRequestProcessed(device: string) { + // TODO: this is called when we process the session request + // This should store the processed timestamp + // Again naming is crap so maybe some other name is better +} diff --git a/ts/session/protocols/index.ts b/ts/session/protocols/index.ts new file mode 100644 index 000000000..e0cfeb680 --- /dev/null +++ b/ts/session/protocols/index.ts @@ -0,0 +1,4 @@ +import * as SessionProtocol from './SessionProtocol'; +import * as MultiDeviceProtocol from './MultiDeviceProtocol'; + +export { SessionProtocol, MultiDeviceProtocol }; diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts new file mode 100644 index 000000000..fb9ed0445 --- /dev/null +++ b/ts/session/sending/MessageQueue.ts @@ -0,0 +1,54 @@ +import { MessageQueueInterface } from './MessageQueueInterface'; +import { OutgoingContentMessage, OpenGroupMessage } from '../messages/outgoing'; +import { JobQueue } from '../utils/JobQueue'; +import { PendingMessageCache } from './PendingMessageCache'; + +export class MessageQueue implements MessageQueueInterface { + private readonly jobQueues: Map = new Map(); + private readonly cache: PendingMessageCache; + + constructor() { + this.cache = new PendingMessageCache(); + this.processAllPending(); + } + + public sendUsingMultiDevice(user: string, message: OutgoingContentMessage) { + throw new Error('Method not implemented.'); + } + public send(device: string, message: OutgoingContentMessage) { + throw new Error('Method not implemented.'); + } + public sendToGroup(message: OutgoingContentMessage | OpenGroupMessage) { + throw new Error('Method not implemented.'); + } + public sendSyncMessage(message: OutgoingContentMessage) { + throw new Error('Method not implemented.'); + } + + public processPending(device: string) { + // TODO: implement + } + + private processAllPending() { + // TODO: Get all devices which are pending here + } + + private queue(device: string, message: OutgoingContentMessage) { + // TODO: implement + } + + private queueOpenGroupMessage(message: OpenGroupMessage) { + // TODO: Do we need to queue open group messages? + // If so we can get open group job queue and add the send job here + } + + private getJobQueue(device: string): JobQueue { + let queue = this.jobQueues.get(device); + if (!queue) { + queue = new JobQueue(); + this.jobQueues.set(device, queue); + } + + return queue; + } +} diff --git a/ts/session/sending/MessageQueueInterface.ts b/ts/session/sending/MessageQueueInterface.ts new file mode 100644 index 000000000..06f52cc7e --- /dev/null +++ b/ts/session/sending/MessageQueueInterface.ts @@ -0,0 +1,13 @@ +import { OutgoingContentMessage, OpenGroupMessage } from '../messages/outgoing'; + +// TODO: add all group messages here, replace OutgoingContentMessage with them +type GroupMessageType = OpenGroupMessage | OutgoingContentMessage; +export interface MessageQueueInterface { + sendUsingMultiDevice(user: string, message: OutgoingContentMessage): void; + send(device: string, message: OutgoingContentMessage): void; + sendToGroup(message: GroupMessageType): void; + sendSyncMessage(message: OutgoingContentMessage): void; + // TODO: Find a good way to handle events in this + // E.g if we do queue.onMessageSent() we want to also be able to stop listening to the event + // TODO: implement events here +} diff --git a/ts/session/sending/MessageSender.ts b/ts/session/sending/MessageSender.ts new file mode 100644 index 000000000..852157ca9 --- /dev/null +++ b/ts/session/sending/MessageSender.ts @@ -0,0 +1,14 @@ +// REMOVE COMMENT AFTER: This can just export pure functions as it doesn't need state + +import { RawMessage } from '../types/RawMessage'; +import { OpenGroupMessage } from '../messages/outgoing'; + +export async function send(message: RawMessage): Promise { + return Promise.resolve(); +} + +export async function sendToOpenGroup( + message: OpenGroupMessage +): Promise { + return Promise.resolve(); +} diff --git a/ts/session/sending/PendingMessageCache.ts b/ts/session/sending/PendingMessageCache.ts new file mode 100644 index 000000000..1d722642b --- /dev/null +++ b/ts/session/sending/PendingMessageCache.ts @@ -0,0 +1,36 @@ +import { RawMessage } from '../types/RawMessage'; +import { OutgoingContentMessage } from '../messages/outgoing'; + +// TODO: We should be able to import functions straight from the db here without going through the window object + +export class PendingMessageCache { + private cachedMessages: Array = []; + + constructor() { + // TODO: We should load pending messages from db here + } + + public addPendingMessage( + device: string, + message: OutgoingContentMessage + ): RawMessage { + // TODO: Maybe have a util for converting OutgoingContentMessage to RawMessage? + // TODO: Raw message has uuid, how are we going to set that? maybe use a different identifier? + // One could be device + timestamp would make a unique identifier + // TODO: Return previous pending message if it exists + return {} as RawMessage; + } + + public removePendingMessage(message: RawMessage) { + // TODO: implement + } + + public getPendingDevices(): Array { + // TODO: this should return all devices which have pending messages + return []; + } + + public getPendingMessages(device: string): Array { + return []; + } +} diff --git a/ts/session/sending/index.ts b/ts/session/sending/index.ts new file mode 100644 index 000000000..69ca9153b --- /dev/null +++ b/ts/session/sending/index.ts @@ -0,0 +1,5 @@ +import * as MessageSender from './MessageSender'; +import { MessageQueue } from './MessageQueue'; +import { MessageQueueInterface } from './MessageQueueInterface'; + +export { MessageSender, MessageQueue, MessageQueueInterface }; diff --git a/ts/session/types/EncryptionType.ts b/ts/session/types/EncryptionType.ts new file mode 100644 index 000000000..ed27e1023 --- /dev/null +++ b/ts/session/types/EncryptionType.ts @@ -0,0 +1,5 @@ +export enum EncryptionType { + Signal, + SessionReset, + MediumGroup, +} diff --git a/ts/session/types/RawMessage.ts b/ts/session/types/RawMessage.ts new file mode 100644 index 000000000..30d2e0d9b --- /dev/null +++ b/ts/session/types/RawMessage.ts @@ -0,0 +1,12 @@ +import { EncryptionType } from './EncryptionType'; + +// TODO: Should we store failure count on raw messages?? +// Might be better to have a seperate interface which takes in a raw message aswell as a failure count +export interface RawMessage { + identifier: string; + plainTextBuffer: Uint8Array; + timestamp: number; + device: string; + ttl: number; + encryption: EncryptionType; +} diff --git a/ts/session/utils/JobQueue.ts b/ts/session/utils/JobQueue.ts new file mode 100644 index 000000000..d9a58d909 --- /dev/null +++ b/ts/session/utils/JobQueue.ts @@ -0,0 +1,40 @@ +import { v4 as uuid } from 'uuid'; + +// TODO: This needs to replace js/modules/job_queue.js +export class JobQueue { + private pending: Promise = Promise.resolve(); + private readonly jobs: Map> = new Map(); + + public has(id: string): boolean { + return this.jobs.has(id); + } + + public async add(job: () => any): Promise { + const id = uuid(); + + return this.addWithId(id, job); + } + + public async addWithId(id: string, job: () => any): Promise { + if (this.jobs.has(id)) { + return this.jobs.get(id); + } + + const previous = this.pending || Promise.resolve(); + this.pending = previous.then(job, job); + + const current = this.pending; + current + .finally(() => { + if (this.pending === current) { + delete this.pending; + } + this.jobs.delete(id); + }) + .ignore(); + + this.jobs.set(id, current); + + return current; + } +}