diff --git a/ts/session/messages/outgoing/content/ContentMessage.ts b/ts/session/messages/outgoing/content/ContentMessage.ts index 926a57e37..ba82bfa40 100644 --- a/ts/session/messages/outgoing/content/ContentMessage.ts +++ b/ts/session/messages/outgoing/content/ContentMessage.ts @@ -5,7 +5,7 @@ export abstract class ContentMessage implements Message { public readonly timestamp: number; public readonly identifier: string; - constructor(timestamp: number, identifier: string) { + constructor({ timestamp, identifier }: { timestamp: number; identifier: string }) { this.timestamp = timestamp; this.identifier = identifier; } diff --git a/ts/session/messages/outgoing/content/DeviceLinkMessage.ts b/ts/session/messages/outgoing/content/DeviceLinkMessage.ts deleted file mode 100644 index b465890b2..000000000 --- a/ts/session/messages/outgoing/content/DeviceLinkMessage.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ContentMessage } from './ContentMessage'; -import { SignalService } from '../../../../protobuf'; - -export class DeviceLinkMessage extends ContentMessage { - - public ttl(): number { - return 2 * 60 * 1000; // 2 minutes for linking requests - } - - protected contentProto(): SignalService.Content { - throw new Error('Not implemented'); - } -} diff --git a/ts/session/messages/outgoing/content/link/DeviceLinkGrantMessage.ts b/ts/session/messages/outgoing/content/link/DeviceLinkGrantMessage.ts new file mode 100644 index 000000000..ce585e28c --- /dev/null +++ b/ts/session/messages/outgoing/content/link/DeviceLinkGrantMessage.ts @@ -0,0 +1,60 @@ +import { SignalService } from '../../../../../protobuf'; +import { DeviceLinkRequestMessage } from './DeviceLinkRequestMessage'; +import { LokiProfile } from '../../../../../types/Message'; + +interface DeviceLinkGrantMessageParams { + timestamp: number; + identifier: string; + + // pairing authorisation + primaryDevicePubKey: string; + secondaryDevicePubKey: string; + requestSignature: Uint8Array; + grantSignature: Uint8Array; + + lokiProfile: LokiProfile; +} + +export class DeviceLinkGrantMessage extends DeviceLinkRequestMessage { + private readonly displayName: string; + private readonly avatarPointer: string; + private readonly profileKey: Uint8Array; + private readonly grantSignature: Uint8Array | null; + + constructor(params: DeviceLinkGrantMessageParams + ) { + super({ + timestamp: params.timestamp, + identifier: params.identifier, + primaryDevicePubKey: params.primaryDevicePubKey, + secondaryDevicePubKey: params.secondaryDevicePubKey, + requestSignature: params.requestSignature, + }); + + this.displayName = params.lokiProfile.displayName; + this.avatarPointer = params.lokiProfile.avatarPointer; + this.profileKey = params.lokiProfile.profileKey; + this.grantSignature = params.grantSignature; + } + + protected getPairingAuthorisationMessage(): SignalService.PairingAuthorisationMessage { + return new SignalService.PairingAuthorisationMessage({ + primaryDevicePubKey: this.primaryDevicePubKey, + secondaryDevicePubKey: this.secondaryDevicePubKey, + requestSignature: this.requestSignature, + grantSignature: this.grantSignature, + }); + } + + protected getDataMessage(): SignalService.DataMessage | null { + // Send profile name to secondary device and avatarPointer + const profile = new SignalService.DataMessage.LokiProfile(); + profile.avatar = this.avatarPointer; + profile.displayName = this.displayName; + + return new SignalService.DataMessage({ + profile, + profileKey: this.profileKey, + }); + } +} diff --git a/ts/session/messages/outgoing/content/link/DeviceLinkRequestMessage.ts b/ts/session/messages/outgoing/content/link/DeviceLinkRequestMessage.ts new file mode 100644 index 000000000..58df123f0 --- /dev/null +++ b/ts/session/messages/outgoing/content/link/DeviceLinkRequestMessage.ts @@ -0,0 +1,49 @@ +import { ContentMessage } from '../ContentMessage'; +import { SignalService } from '../../../../../protobuf'; +interface DeviceLinkMessageParams { + timestamp: number; + identifier: string; + + primaryDevicePubKey: string; + secondaryDevicePubKey: string; + requestSignature: Uint8Array; +} + + +export class DeviceLinkRequestMessage extends ContentMessage { + protected readonly primaryDevicePubKey: string; + protected readonly secondaryDevicePubKey: string; + protected readonly requestSignature: Uint8Array; + + constructor(params: DeviceLinkMessageParams) { + super({ timestamp: params.timestamp, identifier: params.identifier }); + this.primaryDevicePubKey = params.primaryDevicePubKey; + this.secondaryDevicePubKey = params.secondaryDevicePubKey; + this.requestSignature = params.requestSignature; + } + + public ttl(): number { + return 2 * 60 * 1000; // 2 minutes for pairing requests + } + + protected getDataMessage(): SignalService.DataMessage | null { + return null; + } + + protected getPairingAuthorisationMessage(): SignalService.PairingAuthorisationMessage { + return new SignalService.PairingAuthorisationMessage({ + primaryDevicePubKey: this.primaryDevicePubKey, + secondaryDevicePubKey: this.secondaryDevicePubKey, + requestSignature: this.requestSignature, + grantSignature: null, + }); + } + + protected contentProto(): SignalService.Content { + return new SignalService.Content({ + pairingAuthorisation: this.getPairingAuthorisationMessage(), + dataMessage: this.getDataMessage() || null, + }); + } +} + diff --git a/ts/session/messages/outgoing/content/receipt/ReceiptMessage.ts b/ts/session/messages/outgoing/content/receipt/ReceiptMessage.ts index 85ff70461..53fe7e16e 100644 --- a/ts/session/messages/outgoing/content/receipt/ReceiptMessage.ts +++ b/ts/session/messages/outgoing/content/receipt/ReceiptMessage.ts @@ -4,8 +4,9 @@ import { SignalService } from '../../../../../protobuf'; export abstract class ReceiptMessage extends ContentMessage { private readonly timestamps: Array; - constructor(timestamp: number, identifier: string, timestamps: Array) { - super(timestamp, identifier); + constructor({ timestamp, identifier, timestamps }: + { timestamp: number; identifier: string; timestamps: Array }) { + super({timestamp, identifier}); this.timestamps = timestamps; } diff --git a/ts/session/messages/outgoing/index.ts b/ts/session/messages/outgoing/index.ts index 7794de5b4..ba7a3a88d 100644 --- a/ts/session/messages/outgoing/index.ts +++ b/ts/session/messages/outgoing/index.ts @@ -12,7 +12,8 @@ import { RegularMessage } from './content/data/RegularMessage'; import { SessionResetMessage } from './content/SessionResetMessage'; import { SessionEstablishedMessage } from './content/SessionEstablishedMessage'; import { EndSessionMessage } from './content/EndSessionMessage'; -import { DeviceLinkMessage } from './content/DeviceLinkMessage'; +import { DeviceLinkRequestMessage } from './content/link/DeviceLinkRequestMessage'; +import { DeviceLinkGrantMessage } from './content/link/DeviceLinkGrantMessage'; import { ReadReceiptMessage } from './content/receipt/ReadReceiptMessage'; import { DeliveryReceiptMessage } from './content/receipt/DeliveryReceiptMessage'; @@ -23,7 +24,8 @@ export { ContentMessage, // children of ContentMessage - DeviceLinkMessage, + DeviceLinkRequestMessage, + DeviceLinkGrantMessage, EndSessionMessage, ReceiptMessage, ReadReceiptMessage, diff --git a/ts/test/session/messages/DeviceLinkMessage_test.ts b/ts/test/session/messages/DeviceLinkMessage_test.ts new file mode 100644 index 000000000..95577f450 --- /dev/null +++ b/ts/test/session/messages/DeviceLinkMessage_test.ts @@ -0,0 +1,95 @@ +import { expect } from 'chai'; +import { beforeEach} from 'mocha'; + +import { DeviceLinkGrantMessage, DeviceLinkRequestMessage, ContentMessage } from '../../../session/messages/outgoing'; +import { SignalService } from '../../../protobuf'; +import { LokiProfile } from '../../../types/Message'; + +describe('DeviceLinkMessage', () => { + let linkRequestMessage: DeviceLinkRequestMessage; + let linkGrantMessage: DeviceLinkGrantMessage; + let lokiProfile: LokiProfile; + + beforeEach(() => { + linkRequestMessage = new DeviceLinkRequestMessage({ + timestamp: Date.now(), + identifier: '123456', + primaryDevicePubKey: '111111', + secondaryDevicePubKey: '222222', + requestSignature: new Uint8Array([1, 2, 3, 4, 5, 6]), + }); + + lokiProfile = { + displayName: 'displayName', + avatarPointer: 'avatarPointer', + profileKey: new Uint8Array([1, 2, 3, 4]), + }; + + linkGrantMessage = new DeviceLinkGrantMessage({ + timestamp: Date.now(), + identifier: '123456', + primaryDevicePubKey: '111111', + secondaryDevicePubKey: '222222', + requestSignature: new Uint8Array([1, 2, 3, 4, 5, 6]), + grantSignature: new Uint8Array([6, 5, 4, 3, 2, 1]), + lokiProfile, + }); + }); + + describe('content of a linkRequestMessage ', () => { + let decoded: any; + before(() => { + const plainText = linkRequestMessage.plainTextBuffer(); + decoded = SignalService.Content.toObject(SignalService.Content.decode(plainText)); + }); + + it('has a pairingAuthorisation.primaryDevicePubKey', () => { + expect(decoded.pairingAuthorisation).to.have.property('primaryDevicePubKey', '111111'); + }); + it('has a pairingAuthorisation.secondaryDevicePubKey', () => { + expect(decoded.pairingAuthorisation).to.have.property('secondaryDevicePubKey', '222222'); + }); + it('has a pairingAuthorisation.requestSignature', () => { + expect(decoded.pairingAuthorisation).to.have.property('requestSignature').to.deep.equal(new Uint8Array([1, 2, 3, 4, 5, 6])); + }); + it('has no pairingAuthorisation.grantSignature', () => { + expect(decoded.pairingAuthorisation).to.not.have.property('grantSignature'); + }); + it('has no lokiProfile', () => { + expect(decoded).to.not.have.property('lokiProfile'); + }); + }); + + describe('content of a linkGrantMessage ', () => { + let decoded: any; + before(() => { + const plainText = linkGrantMessage.plainTextBuffer(); + decoded = SignalService.Content.toObject(SignalService.Content.decode(plainText)); + }); + + it('has a pairingAuthorisation.primaryDevicePubKey', () => { + expect(decoded.pairingAuthorisation).to.have.property('primaryDevicePubKey', '111111'); + }); + it('has a pairingAuthorisation.secondaryDevicePubKey', () => { + expect(decoded.pairingAuthorisation).to.have.property('secondaryDevicePubKey', '222222'); + }); + it('has a pairingAuthorisation.requestSignature', () => { + expect(decoded.pairingAuthorisation).to.have.property('requestSignature').to.deep.equal(new Uint8Array([1, 2, 3, 4, 5, 6])); + }); + it('has a pairingAuthorisation.grantSignature', () => { + expect(decoded.pairingAuthorisation).to.have.property('grantSignature').to.deep.equal(new Uint8Array([6, 5, 4, 3, 2, 1])); + }); + it('has a lokiProfile', () => { + expect(decoded.dataMessage).to.have.property('profileKey').to.be.deep.equal(lokiProfile.profileKey); + expect(decoded.dataMessage).to.have.property('profile').to.be.deep.equal({ + displayName: 'displayName', + avatar: 'avatarPointer', + }); + }); + }); + + it('ttl of 2 minutes', () => { + expect(linkRequestMessage.ttl()).to.equal(2 * 60 * 1000); + expect(linkGrantMessage.ttl()).to.equal(2 * 60 * 1000); + }); +}); diff --git a/ts/test/session/messages/DeviceUnlinkMessage_test.ts b/ts/test/session/messages/DeviceUnlinkMessage_test.ts index c96971ba9..ce6ed615f 100644 --- a/ts/test/session/messages/DeviceUnlinkMessage_test.ts +++ b/ts/test/session/messages/DeviceUnlinkMessage_test.ts @@ -7,7 +7,9 @@ import { SignalService } from '../../../protobuf'; describe('DeviceUnlinkMessage', () => { let message: DeviceUnlinkMessage; beforeEach(() => { - message = new DeviceUnlinkMessage(Date.now(), '123456'); + const timestamp = Date.now(); + const identifier = '123456'; + message = new DeviceUnlinkMessage({timestamp, identifier}); }); it('content of just the UNPAIRING_REQUEST flag set', () => { diff --git a/ts/test/session/messages/ReceiptMessage_test.ts b/ts/test/session/messages/ReceiptMessage_test.ts index 79d6d660f..c32f04205 100644 --- a/ts/test/session/messages/ReceiptMessage_test.ts +++ b/ts/test/session/messages/ReceiptMessage_test.ts @@ -11,8 +11,10 @@ describe('ReceiptMessage', () => { beforeEach(() => { timestamps = [987654321, 123456789]; - readMessage = new ReadReceiptMessage(Date.now(), '123456', timestamps); - deliveryMessage = new DeliveryReceiptMessage(Date.now(), '123456', timestamps); + const timestamp = Date.now(); + const identifier = '123456'; + readMessage = new ReadReceiptMessage({timestamp, identifier, timestamps}); + deliveryMessage = new DeliveryReceiptMessage({timestamp, identifier, timestamps}); }); it('content of a read receipt is correct', () => { diff --git a/ts/types/Message.ts b/ts/types/Message.ts index 1a2a6baad..d7cb90e63 100644 --- a/ts/types/Message.ts +++ b/ts/types/Message.ts @@ -103,3 +103,10 @@ export const hasExpiration = (message: Message): boolean => { return typeof expireTimer === 'number' && expireTimer > 0; }; + + +export type LokiProfile = { + displayName: string; + avatarPointer: string; + profileKey: Uint8Array; +};