1
- import { IBinary , IMessageJSON , TKeyType } from '../../interfaces' ;
1
+ import { IBinary , IMessageJSON , IMessageMeta , TKeyType } from '../../interfaces' ;
2
2
import Binary from '../Binary' ;
3
3
import { Account , cypher } from '../accounts' ;
4
4
import { concatBytes } from '@noble/hashes/utils' ;
@@ -10,12 +10,20 @@ import {
10
10
bytesToByteArrayWithSize ,
11
11
longToByteArray ,
12
12
stringToByteArrayWithSize ,
13
+ byteToByteArray ,
14
+ booleanToBytes ,
13
15
} 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
15
20
16
21
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 : '' } ;
19
27
20
28
/** Meta type of the data */
21
29
mediaType : string ;
@@ -41,8 +49,11 @@ export default class Message {
41
49
/** Encrypted data */
42
50
private _encryptedData ?: IBinary ;
43
51
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 } ;
46
57
47
58
if ( typeof data === 'string' ) {
48
59
this . mediaType = mediaType ?? 'text/plain' ;
@@ -52,12 +63,15 @@ export default class Message {
52
63
this . data = data instanceof Binary ? data : new Binary ( data ) ;
53
64
} else {
54
65
if ( mediaType && mediaType !== 'application/json' ) throw new Error ( `Unable to encode data as ${ mediaType } ` ) ;
55
-
56
66
this . mediaType = mediaType ?? 'application/json' ;
57
67
this . data = new Binary ( JSON . stringify ( data ) ) ;
58
68
}
59
69
}
60
70
71
+ get type ( ) : string {
72
+ return this . meta . type ; // Backwards compatibility
73
+ }
74
+
61
75
get hash ( ) : Binary {
62
76
return this . _hash ?? new Binary ( this . toBinary ( false ) ) . hash ( ) ;
63
77
}
@@ -103,9 +117,7 @@ export default class Message {
103
117
this . timestamp ??= new Date ( ) ;
104
118
this . sender = { keyType : sender . keyType , publicKey : sender . signKey . publicKey } ;
105
119
this . signature = sender . sign ( this . toBinary ( false ) ) ;
106
-
107
120
this . _hash = this . hash ;
108
-
109
121
return this ;
110
122
}
111
123
@@ -115,44 +127,96 @@ export default class Message {
115
127
116
128
verifySignature ( ) : boolean {
117
129
if ( ! this . signature || ! this . sender ) throw new Error ( 'Message is not signed' ) ;
118
-
119
130
return cypher ( this . sender ) . verifySignature ( this . toBinary ( false ) , this . signature ) ;
120
131
}
121
132
122
133
verifyHash ( ) : boolean {
123
134
return this . _hash === undefined || this . _hash . hex === new Binary ( this . toBinary ( false ) ) . hash ( ) . hex ;
124
135
}
125
136
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
+ }
129
141
130
142
const data = this . _encryptedData
131
143
? bytesToByteArrayWithSize ( this . _encryptedData , 'int32' )
132
- : concatBytes ( stringToByteArrayWithSize ( this . mediaType ) , bytesToByteArrayWithSize ( this . data , 'int32' ) ) ;
144
+ : concatBytes ( stringToByteArrayWithSize ( this . mediaType , 'int16' ) , bytesToByteArrayWithSize ( this . data , 'int32' ) ) ;
133
145
134
146
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 ) ) ,
137
150
this . sender . publicKey ,
138
151
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 ) ,
141
154
data ,
142
- withSignature ? this . signature : new Uint8Array ( 0 ) ,
155
+ withSignature && this . signature ? this . signature : new Uint8Array ( 0 ) ,
143
156
) ;
144
157
}
145
158
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
+
146
200
toJSON ( ) : IMessageJSON {
147
201
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
+ } ,
149
209
sender : this . sender ? { keyType : this . sender . keyType , publicKey : this . sender . publicKey . base58 } : undefined ,
150
210
recipient : this . recipient ,
151
211
timestamp : this . timestamp ,
152
212
signature : this . signature ?. base58 ,
153
213
hash : this . hash . base58 ,
154
214
} ;
155
215
216
+ if ( this . version === MESSAGE_V1 ) {
217
+ ( base as any ) . type = this . meta . type ; // Backwards compatibility
218
+ }
219
+
156
220
return this . _encryptedData
157
221
? { ...base , encryptedData : 'base64:' + this . _encryptedData ?. base64 }
158
222
: { ...base , mediaType : this . mediaType , data : 'base64:' + this . data ?. base64 } ;
@@ -165,7 +229,19 @@ export default class Message {
165
229
private static fromJSON ( json : IMessageJSON ) : Message {
166
230
const message : Message = Object . create ( Message . prototype ) ;
167
231
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
+
169
245
message . sender = {
170
246
keyType : json . sender . keyType ,
171
247
publicKey : Binary . fromBase58 ( json . sender . publicKey ) ,
@@ -194,40 +270,82 @@ export default class Message {
194
270
195
271
private static fromBinary ( data : Uint8Array ) : Message {
196
272
const message : Message = Object . create ( Message . prototype ) ;
273
+ message . meta = { type : '' , title : '' , description : '' } ;
197
274
let offset = 0 ;
198
275
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 ++ ] ;
202
278
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
203
313
const senderKeyType = data [ offset ++ ] ;
204
314
const senderPublicKeyLength = senderKeyType === 1 ? 32 : 33 ;
205
315
const senderPublicKey = data . slice ( offset , offset + senderPublicKeyLength ) ;
206
316
message . sender = { keyType : keyTypeFromId ( senderKeyType ) , publicKey : new Binary ( senderPublicKey ) } ;
207
317
offset += senderPublicKeyLength ;
208
318
319
+ // recipient
209
320
message . recipient = base58 . encode ( data . slice ( offset , offset + 26 ) ) ;
210
321
offset += 26 ;
211
322
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 ) ) ;
213
326
offset += 8 ;
214
327
215
328
const encrypted = data [ offset ++ ] === 1 ;
216
329
217
330
if ( encrypted ) {
331
+ // encrypted data
218
332
message . _encryptedData = new Binary ( byteArrayWithSizeToBytes ( data . slice ( offset ) , 'int32' ) ) ;
219
333
offset += message . _encryptedData . length + 4 ;
220
334
} 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 ) ;
223
338
offset += mediaTypeBytes . length + 2 ;
224
339
340
+ // data
225
341
message . data = new Binary ( byteArrayWithSizeToBytes ( data . slice ( offset ) , 'int32' ) ) ;
226
342
offset += message . data . length + 4 ;
227
343
}
228
344
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
+ }
231
349
232
350
return message ;
233
351
}
0 commit comments