Skip to content

Commit cd3d589

Browse files
authored
Merge pull request #140 from ltonetwork/backport-updates
Message v2
2 parents 5783239 + 475fffb commit cd3d589

File tree

10 files changed

+3254
-229
lines changed

10 files changed

+3254
-229
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ jobs:
6666
provider: npm
6767
6868
api_key:
69-
secure: MsWwbOSQKOXnZYcaIR/8YXuROE20smQA5BY/VbS2uHaoR+jRoTryqcXADdTD48HJcknsTn9CuN978NL2HTKC3hT0UD/a9neY0R96WpaXbxDU8WepU8KI6kTBKjzTlvlpgPvh7KQtuo+Zbzwj7zD/MFT0zQMuxdQJ6pLT2tU863M9puJEnJ4QQsV98xv5SASqe/5ZRpSMHzlsc9wccz+GTpfycmmJDnVpqm/Be8kGfFx5BgzN6DE6H4V87aH3HqTUIr8GvpX39mWBrK4RBCtFz/E2XVDaCBgi9VEEygQZHE9Us9MUV+gaIjSYMVSHkRbg98jd08mVvKK2qQ3hnEWWM3D8Tq7j7Pzg5lfQYiApNOx70Kb1tvIVoBdo4jUPilO3zryd6mminXCnZzFzVkUmSQXdpy4hWEw/4Ed/JMOQQ12um8oGl9u+EhYjDCReVh8/p5QBdhBah1D41E+VI7ejwN5dEpdyi/2MXPbc+cHfDaNVcXhSnG1iFQyUA614CQnA18Pywna12UGQyupgRAApUNwqEHT5oB9gcB9ZH7Q+uy3KcFvC+Fy2Yw6k0ygGNTp+8CCj6hirpREkfINlTLG19qOZArwr8Dkw36Mlo+WOj3v0/Lm68LcMAspT/LluaUNnkSgFU3vt4ffBaefhyKiO5ufxBDyykJMr21ho5xZAdgE=
69+
secure: "AjrFCxtyHSZSimRF/vQr3up8s3oK3gEJZcaIO+KmgxQKhyqWdCU73y9VEm0rF5fd7nMMbK5C80dUNVnUqGszAxUCpbivEwYg+Ymugu2JrhkjTDvrcLbbQei+Z5EbONtGdqPy5WMhqjsFEcqwPA/Ja9Jd5wn6fyP5KWm/EFEfucs5JqA+CfV3YHE6i7tY6y7zlwksP3HAHUEcpDIjIy31z3gmqrsWL5q46EErub7+ugaPZSQYVgYF73xJzrZbqnEbQ5FLOP1oDNL/aEYSW9aOm7rIqlFDwO2XKKYar3U4hhM4C8kuaUrEvCv+bSGPH+zwiDVHxItZtONZRmPEE6alKUb1xokTzGo1vRcg9baSeiuazhUHzRfVJSekXCVkeArerBca/HDc6g9xwanp5FTg+uJ9DwWUfUVyzjjinJogKbx9vvHpJtS0WL72NkgS1ViRYCoEVOHVPJ7esJBN3FM3Q1jizMFh6mLMYylRaR2JpUk2pBGHXoP2J5mJxeVDbOjSe7SZKOPao/sNFQXkw7f/OFR0JF/n8SqTsRbrzneRZj99fZFzJfHZUNi0PsAvFpKQYw48JlhEwOFSqpKNCJcAtjF7p/HphPYnClbl2v3+pj8mpx51uhu2njsMNL5jQgNBhulIf2mMdGTZ7MggIA18y7yjKXqAJmDhoPIudHwSueI="
7070
on:
7171
tags: true
7272
skip_cleanup: true

interfaces.d.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export interface IBinary extends Uint8Array {
4646
hash(): IBinary;
4747
hmac(key: string | Uint8Array): IBinary;
4848
slice(start?: number, end?: number): IBinary;
49-
reverse(): IBinary;
49+
reverse(): this;
5050
toReversed(): IBinary;
5151
}
5252

@@ -80,8 +80,21 @@ export interface IEventJSON {
8080
attachments?: Array<{ name: string; mediaType: string; data: string }>;
8181
}
8282

83-
interface IMessageJSONBase {
83+
export interface IMessageMeta {
8484
type: string;
85+
title: string;
86+
description: string;
87+
thumbnail?: IBinary;
88+
}
89+
90+
interface IMessageJSONBase {
91+
version: number;
92+
meta: {
93+
type: string;
94+
title: string;
95+
description: string;
96+
thumbnail?: string;
97+
};
8598
sender: IPublicAccount;
8699
recipient: string;
87100
timestamp: Date | string;

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,6 @@
6262
},
6363
"engines": {
6464
"node": ">=16.0.0"
65-
}
65+
},
66+
"packageManager": "[email protected]+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
6667
}

src/Binary.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export default class Binary extends Uint8Array implements IBinary {
5050
return new Binary(super.slice(start, end));
5151
}
5252

53-
reverse(): Binary {
53+
reverse(): this {
5454
super.reverse();
5555
return this;
5656
}

src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ export const DEFAULT_MESSAGE_TYPE = 'basic';
1717
export const ASSOCIATION_TYPE_DID_VERIFICATION_METHOD = 0x100;
1818
export const ASSOCIATION_TYPE_DID_DISABLE_CAPABILITY = 0x108;
1919
export const STATEMENT_TYPE_DEACTIVATE_DID = 0x120;
20+
21+
export const MAX_THUMBNAIL_SIZE = 256 * 1024;

src/messages/Message.ts

Lines changed: 147 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IBinary, IMessageJSON, TKeyType } from '../../interfaces';
1+
import { IBinary, IMessageJSON, IMessageMeta, TKeyType } from '../../interfaces';
22
import Binary from '../Binary';
33
import { Account, cypher } from '../accounts';
44
import { concatBytes } from '@noble/hashes/utils';
@@ -10,12 +10,20 @@ import {
1010
bytesToByteArrayWithSize,
1111
longToByteArray,
1212
stringToByteArrayWithSize,
13+
byteToByteArray,
14+
booleanToBytes,
1315
} from '../utils/convert';
14-
import { DEFAULT_MESSAGE_TYPE } from '../constants';
16+
import { DEFAULT_MESSAGE_TYPE, MAX_THUMBNAIL_SIZE } from '../constants';
17+
18+
const MESSAGE_V1 = 0;
19+
const MESSAGE_V2 = 1; // Meta data support
1520

1621
export default class Message {
17-
/** Type of the message */
18-
type: string;
22+
/** Version of the message */
23+
version: number;
24+
25+
/** Extra info and details about the message */
26+
meta: IMessageMeta = { type: DEFAULT_MESSAGE_TYPE, title: '', description: '' };
1927

2028
/** Meta type of the data */
2129
mediaType: string;
@@ -41,8 +49,11 @@ export default class Message {
4149
/** Encrypted data */
4250
private _encryptedData?: IBinary;
4351

44-
constructor(data: any, mediaType?: string, type = DEFAULT_MESSAGE_TYPE) {
45-
this.type = type;
52+
constructor(data: any, mediaType?: string, meta: Partial<IMessageMeta> | string = {}) {
53+
if (typeof meta === 'string') meta = { type: meta }; // Backwards compatibility
54+
55+
this.version = meta.title || meta.description || meta.thumbnail ? MESSAGE_V2 : MESSAGE_V1;
56+
this.meta = { ...this.meta, ...meta };
4657

4758
if (typeof data === 'string') {
4859
this.mediaType = mediaType ?? 'text/plain';
@@ -52,12 +63,15 @@ export default class Message {
5263
this.data = data instanceof Binary ? data : new Binary(data);
5364
} else {
5465
if (mediaType && mediaType !== 'application/json') throw new Error(`Unable to encode data as ${mediaType}`);
55-
5666
this.mediaType = mediaType ?? 'application/json';
5767
this.data = new Binary(JSON.stringify(data));
5868
}
5969
}
6070

71+
get type(): string {
72+
return this.meta.type; // Backwards compatibility
73+
}
74+
6175
get hash(): Binary {
6276
return this._hash ?? new Binary(this.toBinary(false)).hash();
6377
}
@@ -103,9 +117,7 @@ export default class Message {
103117
this.timestamp ??= new Date();
104118
this.sender = { keyType: sender.keyType, publicKey: sender.signKey.publicKey };
105119
this.signature = sender.sign(this.toBinary(false));
106-
107120
this._hash = this.hash;
108-
109121
return this;
110122
}
111123

@@ -115,44 +127,96 @@ export default class Message {
115127

116128
verifySignature(): boolean {
117129
if (!this.signature || !this.sender) throw new Error('Message is not signed');
118-
119130
return cypher(this.sender).verifySignature(this.toBinary(false), this.signature);
120131
}
121132

122133
verifyHash(): boolean {
123134
return this._hash === undefined || this._hash.hex === new Binary(this.toBinary(false)).hash().hex;
124135
}
125136

126-
toBinary(withSignature = true): Uint8Array {
127-
if (!this.recipient) throw new Error('Recipient not set');
128-
if (!this.sender || !this.timestamp || (withSignature && !this.signature)) throw new Error('Message not signed');
137+
private toBinaryV1(withSignature = true): Uint8Array {
138+
if (this.meta?.title || this.meta?.description || this.meta?.thumbnail) {
139+
throw new Error('Meta information is not supported in v1');
140+
}
129141

130142
const data = this._encryptedData
131143
? bytesToByteArrayWithSize(this._encryptedData, 'int32')
132-
: concatBytes(stringToByteArrayWithSize(this.mediaType), bytesToByteArrayWithSize(this.data, 'int32'));
144+
: concatBytes(stringToByteArrayWithSize(this.mediaType, 'int16'), bytesToByteArrayWithSize(this.data, 'int32'));
133145

134146
return concatBytes(
135-
stringToByteArrayWithSize(this.type),
136-
Uint8Array.from([keyTypeId(this.sender.keyType)]),
147+
byteToByteArray(MESSAGE_V1),
148+
stringToByteArrayWithSize(this.meta.type, 'int8'),
149+
byteToByteArray(keyTypeId(this.sender.keyType)),
137150
this.sender.publicKey,
138151
base58.decode(this.recipient),
139-
longToByteArray(this.timestamp.getTime()),
140-
Uint8Array.from([this._encryptedData ? 1 : 0]),
152+
longToByteArray(this.timestamp?.getTime() || 0),
153+
booleanToBytes(!!this._encryptedData),
141154
data,
142-
withSignature ? this.signature : new Uint8Array(0),
155+
withSignature && this.signature ? this.signature : new Uint8Array(0),
143156
);
144157
}
145158

159+
private toBinaryV2(withSignature = true): Uint8Array {
160+
const data = this._encryptedData
161+
? bytesToByteArrayWithSize(this._encryptedData, 'int32')
162+
: concatBytes(stringToByteArrayWithSize(this.mediaType, 'int16'), bytesToByteArrayWithSize(this.data, 'int32'));
163+
164+
return concatBytes(
165+
byteToByteArray(MESSAGE_V2),
166+
stringToByteArrayWithSize(this.meta.type, 'int8'),
167+
stringToByteArrayWithSize(this.meta.title, 'int8'),
168+
stringToByteArrayWithSize(this.meta.description, 'int16'),
169+
bytesToByteArrayWithSize(this.meta.thumbnail || new Uint8Array(0), 'int32'),
170+
byteToByteArray(keyTypeId(this.sender.keyType)),
171+
this.sender.publicKey,
172+
base58.decode(this.recipient),
173+
longToByteArray(this.timestamp?.getTime() || 0),
174+
booleanToBytes(!!this._encryptedData),
175+
data,
176+
withSignature && this.signature ? this.signature : new Uint8Array(0),
177+
);
178+
}
179+
180+
toBinary(withSignature = true): Uint8Array {
181+
if (!this.recipient) {
182+
throw new Error('Recipient not set');
183+
}
184+
185+
if (this.meta.thumbnail?.length > MAX_THUMBNAIL_SIZE) {
186+
throw new Error(`Thumbnail exceeds maximum size of ${MAX_THUMBNAIL_SIZE / 1024} KB`);
187+
}
188+
189+
if (!this.signature && withSignature) {
190+
throw new Error('Message not signed');
191+
}
192+
193+
if (!this.sender || !this.sender.keyType) {
194+
throw new Error('Sender key type is missing');
195+
}
196+
197+
return this.version === MESSAGE_V1 ? this.toBinaryV1(withSignature) : this.toBinaryV2(withSignature);
198+
}
199+
146200
toJSON(): IMessageJSON {
147201
const base = {
148-
type: this.type,
202+
version: this.version,
203+
meta: {
204+
type: this.meta.type,
205+
title: this.meta.title,
206+
description: this.meta.description,
207+
thumbnail: this.meta.thumbnail?.base64,
208+
},
149209
sender: this.sender ? { keyType: this.sender.keyType, publicKey: this.sender.publicKey.base58 } : undefined,
150210
recipient: this.recipient,
151211
timestamp: this.timestamp,
152212
signature: this.signature?.base58,
153213
hash: this.hash.base58,
154214
};
155215

216+
if (this.version === MESSAGE_V1) {
217+
(base as any).type = this.meta.type; // Backwards compatibility
218+
}
219+
156220
return this._encryptedData
157221
? { ...base, encryptedData: 'base64:' + this._encryptedData?.base64 }
158222
: { ...base, mediaType: this.mediaType, data: 'base64:' + this.data?.base64 };
@@ -165,7 +229,19 @@ export default class Message {
165229
private static fromJSON(json: IMessageJSON): Message {
166230
const message: Message = Object.create(Message.prototype);
167231

168-
message.type = json.type;
232+
if ('version' in json && json.version > MESSAGE_V2) {
233+
throw new Error(`Message version ${json.version} not supported`);
234+
}
235+
236+
if (!('version' in json)) {
237+
message.version = MESSAGE_V1;
238+
message.meta = { type: (json as any).type, title: '', description: '' }; // Backwards compatibility
239+
} else {
240+
message.version = json.version;
241+
message.meta = { type: json.meta.type, title: json.meta.title, description: json.meta.description };
242+
message.meta.thumbnail = json.meta.thumbnail ? Binary.fromBase64(json.meta.thumbnail) : undefined;
243+
}
244+
169245
message.sender = {
170246
keyType: json.sender.keyType,
171247
publicKey: Binary.fromBase58(json.sender.publicKey),
@@ -194,40 +270,82 @@ export default class Message {
194270

195271
private static fromBinary(data: Uint8Array): Message {
196272
const message: Message = Object.create(Message.prototype);
273+
message.meta = { type: '', title: '', description: '' };
197274
let offset = 0;
198275

199-
const typeBytes = byteArrayWithSizeToBytes(data.slice(offset));
200-
message.type = new Binary(typeBytes).toString();
201-
offset += typeBytes.length + 2;
276+
// version
277+
message.version = data[offset++];
202278

279+
if (message.version > MESSAGE_V2) {
280+
throw new Error(`Message version ${message.version} not supported`);
281+
}
282+
283+
// meta.type
284+
const typeLength = data[offset++];
285+
const typeBytes = data.slice(offset, offset + typeLength);
286+
message.meta.type = new TextDecoder().decode(typeBytes);
287+
offset += typeLength;
288+
289+
if (message.version === MESSAGE_V2) {
290+
// meta.title
291+
const titleLength = data[offset++];
292+
const titleBytes = data.slice(offset, offset + titleLength);
293+
message.meta.title = new TextDecoder().decode(titleBytes);
294+
offset += titleLength;
295+
296+
// meta.description
297+
const descriptionLength = byteArrayToLong(data.slice(offset, offset + 2));
298+
offset += 2;
299+
const descriptionBytes = data.slice(offset, offset + descriptionLength);
300+
message.meta.description = new TextDecoder().decode(descriptionBytes);
301+
offset += descriptionLength;
302+
303+
// meta.thumbnail
304+
const thumbnailLength = byteArrayToLong(data.slice(offset, offset + 4));
305+
offset += 4;
306+
if (thumbnailLength > 0) {
307+
message.meta.thumbnail = new Binary(data.slice(offset, offset + thumbnailLength));
308+
offset += thumbnailLength;
309+
}
310+
}
311+
312+
// sender
203313
const senderKeyType = data[offset++];
204314
const senderPublicKeyLength = senderKeyType === 1 ? 32 : 33;
205315
const senderPublicKey = data.slice(offset, offset + senderPublicKeyLength);
206316
message.sender = { keyType: keyTypeFromId(senderKeyType), publicKey: new Binary(senderPublicKey) };
207317
offset += senderPublicKeyLength;
208318

319+
// recipient
209320
message.recipient = base58.encode(data.slice(offset, offset + 26));
210321
offset += 26;
211322

212-
message.timestamp = new Date(byteArrayToLong(data.slice(offset, offset + 8)));
323+
// timestamp
324+
const timestampBytes = data.slice(offset, offset + 8);
325+
message.timestamp = new Date(byteArrayToLong(timestampBytes));
213326
offset += 8;
214327

215328
const encrypted = data[offset++] === 1;
216329

217330
if (encrypted) {
331+
// encrypted data
218332
message._encryptedData = new Binary(byteArrayWithSizeToBytes(data.slice(offset), 'int32'));
219333
offset += message._encryptedData.length + 4;
220334
} else {
221-
const mediaTypeBytes = byteArrayWithSizeToBytes(data.slice(offset));
222-
message.mediaType = new Binary(mediaTypeBytes).toString();
335+
// mediaType
336+
const mediaTypeBytes = byteArrayWithSizeToBytes(data.slice(offset), 'int16');
337+
message.mediaType = new TextDecoder().decode(mediaTypeBytes);
223338
offset += mediaTypeBytes.length + 2;
224339

340+
// data
225341
message.data = new Binary(byteArrayWithSizeToBytes(data.slice(offset), 'int32'));
226342
offset += message.data.length + 4;
227343
}
228344

229-
const signature = data.slice(offset);
230-
if (signature.length > 0) message.signature = new Binary(signature);
345+
// signature
346+
if (offset < data.length) {
347+
message.signature = new Binary(data.slice(offset));
348+
}
231349

232350
return message;
233351
}

src/utils/bytes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ export function int16ToBytes(value: number): Uint8Array {
3939
return bytes;
4040
}
4141

42+
// Uses big endian
43+
export function int8ToBytes(value: number): Uint8Array {
44+
const bytes = new Uint8Array(1);
45+
bytes[0] = value & 0xff;
46+
return bytes;
47+
}
48+
4249
export function bytesToInt(bytes: Uint8Array): number {
4350
let value = 0;
4451
for (let i = 0; i < bytes.length; i++) {

0 commit comments

Comments
 (0)