From 0c37e3597c2b6e97fd5339d588b275b001f22142 Mon Sep 17 00:00:00 2001 From: Martin Kobetic Date: Fri, 25 Feb 2022 10:43:52 -0500 Subject: [PATCH] fix: header serialization non-determinism (#73) --- src/Message.ts | 74 ++++++++-------- src/proto/messaging.proto | 14 +-- src/proto/messaging.ts | 176 +++++++++++++++++++------------------- 3 files changed, 135 insertions(+), 129 deletions(-) diff --git a/src/Message.ts b/src/Message.ts index 67a3e3c05..c500ab79f 100644 --- a/src/Message.ts +++ b/src/Message.ts @@ -15,7 +15,8 @@ import { sha256 } from './crypto/encryption' // Message header carries the sender and recipient keys used to protect message. // Message timestamp is set by the sender. export default class Message implements proto.Message { - header: proto.Message_Header | undefined // eslint-disable-line camelcase + header: proto.MessageHeader | undefined // eslint-disable-line camelcase + headerBytes: Uint8Array // encoded header bytes ciphertext: Ciphertext | undefined decrypted: string | undefined error?: Error @@ -27,10 +28,16 @@ export default class Message implements proto.Message { id: string private bytes: Uint8Array - constructor(id: string, bytes: Uint8Array, obj: proto.Message) { + constructor( + id: string, + bytes: Uint8Array, + obj: proto.Message, + header: proto.MessageHeader + ) { this.id = id this.bytes = bytes - this.header = obj.header + this.headerBytes = obj.headerBytes + this.header = header if (obj.ciphertext) { this.ciphertext = new Ciphertext(obj.ciphertext) } @@ -40,14 +47,19 @@ export default class Message implements proto.Message { return this.bytes } - static async create(obj: proto.Message): Promise { - const bytes = proto.Message.encode(obj).finish() + static async create( + obj: proto.Message, + header: proto.MessageHeader, + bytes: Uint8Array + ): Promise { const id = bytesToHex(await sha256(bytes)) - return new Message(id, bytes, obj) + return new Message(id, bytes, obj, header) } static async fromBytes(bytes: Uint8Array): Promise { - return Message.create(proto.Message.decode(bytes)) + const msg = proto.Message.decode(bytes) + const header = proto.MessageHeader.decode(msg.headerBytes) + return Message.create(msg, header, bytes) } get text(): string | undefined { @@ -85,7 +97,7 @@ export default class Message implements proto.Message { message: string, timestamp: Date ): Promise { - const bytes = new TextEncoder().encode(message) + const msgBytes = new TextEncoder().encode(message) const secret = await sender.sharedSecret( recipient, @@ -93,18 +105,16 @@ export default class Message implements proto.Message { false ) // eslint-disable-next-line camelcase - const header: proto.Message_Header = { + const header: proto.MessageHeader = { sender: sender.getPublicKeyBundle(), recipient, timestamp: timestamp.getTime(), } - const headerBytes = proto.Message_Header.encode(header).finish() - const ciphertext = await encrypt(bytes, secret, headerBytes) - - const msg = await Message.create({ - header, - ciphertext, - }) + const headerBytes = proto.MessageHeader.encode(header).finish() + const ciphertext = await encrypt(msgBytes, secret, headerBytes) + const protoMsg = { headerBytes: headerBytes, ciphertext } + const bytes = proto.Message.encode(protoMsg).finish() + const msg = await Message.create(protoMsg, header, bytes) msg.decrypted = message return msg } @@ -117,45 +127,41 @@ export default class Message implements proto.Message { bytes: Uint8Array ): Promise { const message = proto.Message.decode(bytes) - if (!message.header) { + const header = proto.MessageHeader.decode(message.headerBytes) + if (!header) { throw new Error('missing message header') } - if (!message.header.sender) { + if (!header.sender) { throw new Error('missing message sender') } - if (!message.header.sender.identityKey) { + if (!header.sender.identityKey) { throw new Error('missing message sender identity key') } - if (!message.header.sender.preKey) { + if (!header.sender.preKey) { throw new Error('missing message sender pre-key') } - if (!message.header.recipient) { + if (!header.recipient) { throw new Error('missing message recipient') } - if (!message.header.recipient.identityKey) { + if (!header.recipient.identityKey) { throw new Error('missing message recipient identity-key') } - if (!message.header.recipient.preKey) { + if (!header.recipient.preKey) { throw new Error('missing message recipient pre-key') } const recipient = new PublicKeyBundle( - new PublicKey(message.header.recipient.identityKey), - new PublicKey(message.header.recipient.preKey) + new PublicKey(header.recipient.identityKey), + new PublicKey(header.recipient.preKey) ) const sender = new PublicKeyBundle( - new PublicKey(message.header.sender.identityKey), - new PublicKey(message.header.sender.preKey) + new PublicKey(header.sender.identityKey), + new PublicKey(header.sender.preKey) ) - const headerBytes = proto.Message_Header.encode({ - sender: sender, - recipient: recipient, - timestamp: message.header.timestamp, - }).finish() if (!message.ciphertext?.aes256GcmHkdfSha256) { throw new Error('missing message ciphertext') } const ciphertext = new Ciphertext(message.ciphertext) - const msg = await Message.create(message) + const msg = await Message.create(message, header, bytes) let secret: Uint8Array try { if (viewer.identityKey.matches(sender.identityKey)) { @@ -172,7 +178,7 @@ export default class Message implements proto.Message { msg.error = e return msg } - bytes = await decrypt(ciphertext, secret, headerBytes) + bytes = await decrypt(ciphertext, secret, message.headerBytes) msg.decrypted = new TextDecoder().decode(bytes) return msg } diff --git a/src/proto/messaging.proto b/src/proto/messaging.proto index d1d475ec6..9e2cae3c9 100644 --- a/src/proto/messaging.proto +++ b/src/proto/messaging.proto @@ -48,14 +48,14 @@ message PublicKeyBundle { PublicKey preKey = 2; } -message Message { - message Header { - PublicKeyBundle sender = 1; - PublicKeyBundle recipient = 2; - uint64 timestamp = 3; - } +message MessageHeader { + PublicKeyBundle sender = 1; + PublicKeyBundle recipient = 2; + uint64 timestamp = 3; +} - Header header = 1; +message Message { + bytes headerBytes = 1; // encapsulates the encoded MessageHeader Ciphertext ciphertext = 2; } diff --git a/src/proto/messaging.ts b/src/proto/messaging.ts index 99422ca44..adc7da288 100644 --- a/src/proto/messaging.ts +++ b/src/proto/messaging.ts @@ -52,17 +52,18 @@ export interface PublicKeyBundle { preKey: PublicKey | undefined } -export interface Message { - header: Message_Header | undefined - ciphertext: Ciphertext | undefined -} - -export interface Message_Header { +export interface MessageHeader { sender: PublicKeyBundle | undefined recipient: PublicKeyBundle | undefined timestamp: number } +export interface Message { + /** encapsulates the encoded MessageHeader */ + headerBytes: Uint8Array + ciphertext: Ciphertext | undefined +} + export interface PrivateKeyBundle { identityKey: PrivateKey | undefined preKeys: PrivateKey[] @@ -776,36 +777,45 @@ export const PublicKeyBundle = { }, } -function createBaseMessage(): Message { - return { header: undefined, ciphertext: undefined } +function createBaseMessageHeader(): MessageHeader { + return { sender: undefined, recipient: undefined, timestamp: 0 } } -export const Message = { +export const MessageHeader = { encode( - message: Message, + message: MessageHeader, writer: _m0.Writer = _m0.Writer.create() ): _m0.Writer { - if (message.header !== undefined) { - Message_Header.encode(message.header, writer.uint32(10).fork()).ldelim() + if (message.sender !== undefined) { + PublicKeyBundle.encode(message.sender, writer.uint32(10).fork()).ldelim() } - if (message.ciphertext !== undefined) { - Ciphertext.encode(message.ciphertext, writer.uint32(18).fork()).ldelim() + if (message.recipient !== undefined) { + PublicKeyBundle.encode( + message.recipient, + writer.uint32(18).fork() + ).ldelim() + } + if (message.timestamp !== 0) { + writer.uint32(24).uint64(message.timestamp) } return writer }, - decode(input: _m0.Reader | Uint8Array, length?: number): Message { + decode(input: _m0.Reader | Uint8Array, length?: number): MessageHeader { const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input) let end = length === undefined ? reader.len : reader.pos + length - const message = createBaseMessage() + const message = createBaseMessageHeader() while (reader.pos < end) { const tag = reader.uint32() switch (tag >>> 3) { case 1: - message.header = Message_Header.decode(reader, reader.uint32()) + message.sender = PublicKeyBundle.decode(reader, reader.uint32()) break case 2: - message.ciphertext = Ciphertext.decode(reader, reader.uint32()) + message.recipient = PublicKeyBundle.decode(reader, reader.uint32()) + break + case 3: + message.timestamp = longToNumber(reader.uint64() as Long) break default: reader.skipType(tag & 7) @@ -815,83 +825,80 @@ export const Message = { return message }, - fromJSON(object: any): Message { + fromJSON(object: any): MessageHeader { return { - header: isSet(object.header) - ? Message_Header.fromJSON(object.header) + sender: isSet(object.sender) + ? PublicKeyBundle.fromJSON(object.sender) : undefined, - ciphertext: isSet(object.ciphertext) - ? Ciphertext.fromJSON(object.ciphertext) + recipient: isSet(object.recipient) + ? PublicKeyBundle.fromJSON(object.recipient) : undefined, + timestamp: isSet(object.timestamp) ? Number(object.timestamp) : 0, } }, - toJSON(message: Message): unknown { + toJSON(message: MessageHeader): unknown { const obj: any = {} - message.header !== undefined && - (obj.header = message.header - ? Message_Header.toJSON(message.header) + message.sender !== undefined && + (obj.sender = message.sender + ? PublicKeyBundle.toJSON(message.sender) : undefined) - message.ciphertext !== undefined && - (obj.ciphertext = message.ciphertext - ? Ciphertext.toJSON(message.ciphertext) + message.recipient !== undefined && + (obj.recipient = message.recipient + ? PublicKeyBundle.toJSON(message.recipient) : undefined) + message.timestamp !== undefined && + (obj.timestamp = Math.round(message.timestamp)) return obj }, - fromPartial, I>>(object: I): Message { - const message = createBaseMessage() - message.header = - object.header !== undefined && object.header !== null - ? Message_Header.fromPartial(object.header) + fromPartial, I>>( + object: I + ): MessageHeader { + const message = createBaseMessageHeader() + message.sender = + object.sender !== undefined && object.sender !== null + ? PublicKeyBundle.fromPartial(object.sender) : undefined - message.ciphertext = - object.ciphertext !== undefined && object.ciphertext !== null - ? Ciphertext.fromPartial(object.ciphertext) + message.recipient = + object.recipient !== undefined && object.recipient !== null + ? PublicKeyBundle.fromPartial(object.recipient) : undefined + message.timestamp = object.timestamp ?? 0 return message }, } -function createBaseMessage_Header(): Message_Header { - return { sender: undefined, recipient: undefined, timestamp: 0 } +function createBaseMessage(): Message { + return { headerBytes: new Uint8Array(), ciphertext: undefined } } -export const Message_Header = { +export const Message = { encode( - message: Message_Header, + message: Message, writer: _m0.Writer = _m0.Writer.create() ): _m0.Writer { - if (message.sender !== undefined) { - PublicKeyBundle.encode(message.sender, writer.uint32(10).fork()).ldelim() - } - if (message.recipient !== undefined) { - PublicKeyBundle.encode( - message.recipient, - writer.uint32(18).fork() - ).ldelim() + if (message.headerBytes.length !== 0) { + writer.uint32(10).bytes(message.headerBytes) } - if (message.timestamp !== 0) { - writer.uint32(24).uint64(message.timestamp) + if (message.ciphertext !== undefined) { + Ciphertext.encode(message.ciphertext, writer.uint32(18).fork()).ldelim() } return writer }, - decode(input: _m0.Reader | Uint8Array, length?: number): Message_Header { + decode(input: _m0.Reader | Uint8Array, length?: number): Message { const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input) let end = length === undefined ? reader.len : reader.pos + length - const message = createBaseMessage_Header() + const message = createBaseMessage() while (reader.pos < end) { const tag = reader.uint32() switch (tag >>> 3) { case 1: - message.sender = PublicKeyBundle.decode(reader, reader.uint32()) + message.headerBytes = reader.bytes() break case 2: - message.recipient = PublicKeyBundle.decode(reader, reader.uint32()) - break - case 3: - message.timestamp = longToNumber(reader.uint64() as Long) + message.ciphertext = Ciphertext.decode(reader, reader.uint32()) break default: reader.skipType(tag & 7) @@ -901,46 +908,39 @@ export const Message_Header = { return message }, - fromJSON(object: any): Message_Header { + fromJSON(object: any): Message { return { - sender: isSet(object.sender) - ? PublicKeyBundle.fromJSON(object.sender) - : undefined, - recipient: isSet(object.recipient) - ? PublicKeyBundle.fromJSON(object.recipient) + headerBytes: isSet(object.headerBytes) + ? bytesFromBase64(object.headerBytes) + : new Uint8Array(), + ciphertext: isSet(object.ciphertext) + ? Ciphertext.fromJSON(object.ciphertext) : undefined, - timestamp: isSet(object.timestamp) ? Number(object.timestamp) : 0, } }, - toJSON(message: Message_Header): unknown { + toJSON(message: Message): unknown { const obj: any = {} - message.sender !== undefined && - (obj.sender = message.sender - ? PublicKeyBundle.toJSON(message.sender) - : undefined) - message.recipient !== undefined && - (obj.recipient = message.recipient - ? PublicKeyBundle.toJSON(message.recipient) + message.headerBytes !== undefined && + (obj.headerBytes = base64FromBytes( + message.headerBytes !== undefined + ? message.headerBytes + : new Uint8Array() + )) + message.ciphertext !== undefined && + (obj.ciphertext = message.ciphertext + ? Ciphertext.toJSON(message.ciphertext) : undefined) - message.timestamp !== undefined && - (obj.timestamp = Math.round(message.timestamp)) return obj }, - fromPartial, I>>( - object: I - ): Message_Header { - const message = createBaseMessage_Header() - message.sender = - object.sender !== undefined && object.sender !== null - ? PublicKeyBundle.fromPartial(object.sender) - : undefined - message.recipient = - object.recipient !== undefined && object.recipient !== null - ? PublicKeyBundle.fromPartial(object.recipient) + fromPartial, I>>(object: I): Message { + const message = createBaseMessage() + message.headerBytes = object.headerBytes ?? new Uint8Array() + message.ciphertext = + object.ciphertext !== undefined && object.ciphertext !== null + ? Ciphertext.fromPartial(object.ciphertext) : undefined - message.timestamp = object.timestamp ?? 0 return message }, }