Skip to content

Commit 0c37e35

Browse files
authored
fix: header serialization non-determinism (#73)
1 parent acbc4c7 commit 0c37e35

File tree

3 files changed

+135
-129
lines changed

3 files changed

+135
-129
lines changed

src/Message.ts

+40-34
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import { sha256 } from './crypto/encryption'
1515
// Message header carries the sender and recipient keys used to protect message.
1616
// Message timestamp is set by the sender.
1717
export default class Message implements proto.Message {
18-
header: proto.Message_Header | undefined // eslint-disable-line camelcase
18+
header: proto.MessageHeader | undefined // eslint-disable-line camelcase
19+
headerBytes: Uint8Array // encoded header bytes
1920
ciphertext: Ciphertext | undefined
2021
decrypted: string | undefined
2122
error?: Error
@@ -27,10 +28,16 @@ export default class Message implements proto.Message {
2728
id: string
2829
private bytes: Uint8Array
2930

30-
constructor(id: string, bytes: Uint8Array, obj: proto.Message) {
31+
constructor(
32+
id: string,
33+
bytes: Uint8Array,
34+
obj: proto.Message,
35+
header: proto.MessageHeader
36+
) {
3137
this.id = id
3238
this.bytes = bytes
33-
this.header = obj.header
39+
this.headerBytes = obj.headerBytes
40+
this.header = header
3441
if (obj.ciphertext) {
3542
this.ciphertext = new Ciphertext(obj.ciphertext)
3643
}
@@ -40,14 +47,19 @@ export default class Message implements proto.Message {
4047
return this.bytes
4148
}
4249

43-
static async create(obj: proto.Message): Promise<Message> {
44-
const bytes = proto.Message.encode(obj).finish()
50+
static async create(
51+
obj: proto.Message,
52+
header: proto.MessageHeader,
53+
bytes: Uint8Array
54+
): Promise<Message> {
4555
const id = bytesToHex(await sha256(bytes))
46-
return new Message(id, bytes, obj)
56+
return new Message(id, bytes, obj, header)
4757
}
4858

4959
static async fromBytes(bytes: Uint8Array): Promise<Message> {
50-
return Message.create(proto.Message.decode(bytes))
60+
const msg = proto.Message.decode(bytes)
61+
const header = proto.MessageHeader.decode(msg.headerBytes)
62+
return Message.create(msg, header, bytes)
5163
}
5264

5365
get text(): string | undefined {
@@ -85,26 +97,24 @@ export default class Message implements proto.Message {
8597
message: string,
8698
timestamp: Date
8799
): Promise<Message> {
88-
const bytes = new TextEncoder().encode(message)
100+
const msgBytes = new TextEncoder().encode(message)
89101

90102
const secret = await sender.sharedSecret(
91103
recipient,
92104
sender.getCurrentPreKey().publicKey,
93105
false
94106
)
95107
// eslint-disable-next-line camelcase
96-
const header: proto.Message_Header = {
108+
const header: proto.MessageHeader = {
97109
sender: sender.getPublicKeyBundle(),
98110
recipient,
99111
timestamp: timestamp.getTime(),
100112
}
101-
const headerBytes = proto.Message_Header.encode(header).finish()
102-
const ciphertext = await encrypt(bytes, secret, headerBytes)
103-
104-
const msg = await Message.create({
105-
header,
106-
ciphertext,
107-
})
113+
const headerBytes = proto.MessageHeader.encode(header).finish()
114+
const ciphertext = await encrypt(msgBytes, secret, headerBytes)
115+
const protoMsg = { headerBytes: headerBytes, ciphertext }
116+
const bytes = proto.Message.encode(protoMsg).finish()
117+
const msg = await Message.create(protoMsg, header, bytes)
108118
msg.decrypted = message
109119
return msg
110120
}
@@ -117,45 +127,41 @@ export default class Message implements proto.Message {
117127
bytes: Uint8Array
118128
): Promise<Message> {
119129
const message = proto.Message.decode(bytes)
120-
if (!message.header) {
130+
const header = proto.MessageHeader.decode(message.headerBytes)
131+
if (!header) {
121132
throw new Error('missing message header')
122133
}
123-
if (!message.header.sender) {
134+
if (!header.sender) {
124135
throw new Error('missing message sender')
125136
}
126-
if (!message.header.sender.identityKey) {
137+
if (!header.sender.identityKey) {
127138
throw new Error('missing message sender identity key')
128139
}
129-
if (!message.header.sender.preKey) {
140+
if (!header.sender.preKey) {
130141
throw new Error('missing message sender pre-key')
131142
}
132-
if (!message.header.recipient) {
143+
if (!header.recipient) {
133144
throw new Error('missing message recipient')
134145
}
135-
if (!message.header.recipient.identityKey) {
146+
if (!header.recipient.identityKey) {
136147
throw new Error('missing message recipient identity-key')
137148
}
138-
if (!message.header.recipient.preKey) {
149+
if (!header.recipient.preKey) {
139150
throw new Error('missing message recipient pre-key')
140151
}
141152
const recipient = new PublicKeyBundle(
142-
new PublicKey(message.header.recipient.identityKey),
143-
new PublicKey(message.header.recipient.preKey)
153+
new PublicKey(header.recipient.identityKey),
154+
new PublicKey(header.recipient.preKey)
144155
)
145156
const sender = new PublicKeyBundle(
146-
new PublicKey(message.header.sender.identityKey),
147-
new PublicKey(message.header.sender.preKey)
157+
new PublicKey(header.sender.identityKey),
158+
new PublicKey(header.sender.preKey)
148159
)
149-
const headerBytes = proto.Message_Header.encode({
150-
sender: sender,
151-
recipient: recipient,
152-
timestamp: message.header.timestamp,
153-
}).finish()
154160
if (!message.ciphertext?.aes256GcmHkdfSha256) {
155161
throw new Error('missing message ciphertext')
156162
}
157163
const ciphertext = new Ciphertext(message.ciphertext)
158-
const msg = await Message.create(message)
164+
const msg = await Message.create(message, header, bytes)
159165
let secret: Uint8Array
160166
try {
161167
if (viewer.identityKey.matches(sender.identityKey)) {
@@ -172,7 +178,7 @@ export default class Message implements proto.Message {
172178
msg.error = e
173179
return msg
174180
}
175-
bytes = await decrypt(ciphertext, secret, headerBytes)
181+
bytes = await decrypt(ciphertext, secret, message.headerBytes)
176182
msg.decrypted = new TextDecoder().decode(bytes)
177183
return msg
178184
}

src/proto/messaging.proto

+7-7
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,14 @@ message PublicKeyBundle {
4848
PublicKey preKey = 2;
4949
}
5050

51-
message Message {
52-
message Header {
53-
PublicKeyBundle sender = 1;
54-
PublicKeyBundle recipient = 2;
55-
uint64 timestamp = 3;
56-
}
51+
message MessageHeader {
52+
PublicKeyBundle sender = 1;
53+
PublicKeyBundle recipient = 2;
54+
uint64 timestamp = 3;
55+
}
5756

58-
Header header = 1;
57+
message Message {
58+
bytes headerBytes = 1; // encapsulates the encoded MessageHeader
5959
Ciphertext ciphertext = 2;
6060
}
6161

0 commit comments

Comments
 (0)