Skip to content

Commit b61d875

Browse files
fboucquezFernando
authored and
Fernando
committed
fix: improved message api
fix: Improved Crypto unit testing fix: Moved "decode" to Convert.hexToUtf8 fix: EncryptedMessage payload wasn't reproducible.
1 parent f8fe7d8 commit b61d875

17 files changed

+314
-158
lines changed

Diff for: CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ SDK Core| v1.0.3 | [symbol-sdk](https://www.npmjs.com/package/symbol-sdk)
1313
Catbuffer | v1.0.1 | [catbuffer-typescript](https://www.npmjs.com/package/catbuffer-typescript)
1414
Client Library | v1.0.2 | [symbol-openapi-typescript-fetch-client](https://www.npmjs.com/package/symbol-openapi-typescript-fetch-client)
1515

16-
- fix: replaced `instanceof` statements. These statements are problematic when npm installs the dependency in multiples modules.
16+
- fix: Improved message API.
17+
- fix: EncryptedMessage payload wasn't reproducible.
18+
- fix: replaced `instanceof` statements. These statements are problematic when npm installs the dependency in multiples modules.
1719

1820
## [1.0.2] - 25-Oct-2021
1921

Diff for: src/core/crypto/Crypto.ts

+15-9
Original file line numberDiff line numberDiff line change
@@ -107,19 +107,25 @@ export class Crypto {
107107
/**
108108
* Encode a message using AES-GCM algorithm
109109
*
110-
* @param {string} senderPriv - A sender private key
111-
* @param {string} recipientPub - A recipient public key
112-
* @param {string} msg - A text message
113-
* @param {boolean} isHexString - Is payload string a hexadecimal string (default = false)
110+
* @param senderPriv - A sender private key
111+
* @param recipientPub - A recipient public key
112+
* @param msg - A text message
113+
* @param isHexString - Is payload string a hexadecimal string (default = false)
114+
* @param iv - the iv for unit testing, otherwise a random 12 byte array.
115+
*
114116
* @return {string} - The encoded message
115117
*/
116-
public static encode = (senderPriv: string, recipientPub: string, msg: string, isHexString = false): string => {
118+
public static encode = (
119+
senderPriv: string,
120+
recipientPub: string,
121+
msg: string,
122+
isHexString = false,
123+
iv = Crypto.randomBytes(12),
124+
): string => {
117125
// Errors
118126
if (!senderPriv || !recipientPub || !msg) {
119127
throw new Error('Missing argument !');
120128
}
121-
// Processing
122-
const iv = Crypto.randomBytes(12);
123129
const encoded = Crypto._encode(senderPriv, recipientPub, isHexString ? msg : convert.utf8ToHex(msg), iv);
124130
// Result
125131
return encoded;
@@ -171,7 +177,7 @@ export class Crypto {
171177
try {
172178
const decoded = Crypto._decode(recipientPrivate, senderPublic, payloadBuffer, tagAndIv);
173179
return decoded.toUpperCase();
174-
} catch {
180+
} catch (e) {
175181
// To return empty string rather than error throwing if authentication failed
176182
return '';
177183
}
@@ -183,7 +189,7 @@ export class Crypto {
183189
*
184190
* @return {Uint8Array}
185191
*/
186-
public static randomBytes = (length: number): any => {
192+
public static randomBytes = (length: number): Buffer => {
187193
return crypto.randomBytes(length);
188194
};
189195
}

Diff for: src/core/format/Convert.ts

+33-1
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,26 @@ export class Convert {
181181
* @return {string}
182182
*/
183183
public static utf8ToHex = (input: string): string => {
184-
return Buffer.from(input, 'utf-8').toString('hex').toUpperCase();
184+
return Convert.utf8ToBuffer(input).toString('hex').toUpperCase();
185185
};
186186

187+
/**
188+
* Convert hex to UTF-8
189+
* @param {string} hex - an hex string
190+
* @return {string} An UTF-8 string
191+
*/
192+
public static hexToUtf8 = (hex: string): string => {
193+
return Buffer.from(hex, 'hex').toString();
194+
};
195+
/**
196+
* Convert UTF-8 to buffer
197+
* @param input - An UTF-8 string
198+
* @return the buffer
199+
*/
200+
public static utf8ToBuffer(input: string): Buffer {
201+
return Buffer.from(input, 'utf-8');
202+
}
203+
187204
/**
188205
* Convert UTF-8 string to Uint8Array
189206
* @param {string} input - An string with UTF-8 encoding
@@ -266,4 +283,19 @@ export class Convert {
266283
}
267284
return value >>> 0;
268285
}
286+
/**
287+
* It concats a list of Uint8Array into a new one.
288+
*
289+
* @param arrays - the Uint8Array to concat.
290+
*/
291+
public static concat(...arrays: Uint8Array[]): Uint8Array {
292+
const totalLength = arrays.reduce((acc, value) => acc + value.length, 0);
293+
const result = new Uint8Array(totalLength);
294+
let length = 0;
295+
for (const array of arrays) {
296+
result.set(array, length);
297+
length += array.length;
298+
}
299+
return result;
300+
}
269301
}

Diff for: src/model/message/EncryptedMessage.ts

+61-18
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { GeneratorUtils } from 'catbuffer-typescript';
18+
import { Convert } from '../../core';
1719
import { Crypto } from '../../core/crypto';
1820
import { PublicAccount } from '../account';
1921
import { Message } from './Message';
@@ -23,26 +25,40 @@ import { PlainMessage } from './PlainMessage';
2325
/**
2426
* Encrypted Message model
2527
*/
26-
export class EncryptedMessage extends Message {
27-
public readonly recipientPublicAccount?: PublicAccount;
28+
export class EncryptedMessage implements Message {
29+
public readonly type = MessageType.EncryptedMessage;
30+
public readonly payload: string;
2831

29-
constructor(payload: string, recipientPublicAccount?: PublicAccount) {
30-
super(MessageType.EncryptedMessage, payload);
31-
this.recipientPublicAccount = recipientPublicAccount;
32+
/**
33+
* @internal
34+
* @param buffer the buffer.
35+
*/
36+
constructor(private readonly buffer: Uint8Array) {
37+
this.payload = EncryptedMessage.getPayload(buffer);
3238
}
3339

3440
/**
3541
*
3642
* @param message - Plain message to be encrypted
3743
* @param recipientPublicAccount - Recipient public account
3844
* @param privateKey - Sender private key
39-
* @return {EncryptedMessage}
45+
* @param iv - iv for encoding, for unit tests.
46+
* @return The encrypted message.
47+
*/
48+
public static create(message: string, recipientPublicAccount: PublicAccount, privateKey: string, iv?: Buffer): EncryptedMessage {
49+
const encryptedHex = Crypto.encode(privateKey, recipientPublicAccount.publicKey, message, false, iv).toUpperCase();
50+
return new EncryptedMessage(EncryptedMessage.createBuffer(encryptedHex));
51+
}
52+
53+
/**
54+
*
55+
* @param encryptMessage - Encrypted message to be decrypted
56+
* @param privateKey - Recipient private key
57+
* @param recipientPublicAccount - Sender public account
58+
* @return {PlainMessage}
4059
*/
41-
public static create(message: string, recipientPublicAccount: PublicAccount, privateKey: string): EncryptedMessage {
42-
return new EncryptedMessage(
43-
Crypto.encode(privateKey, recipientPublicAccount.publicKey, message).toUpperCase(),
44-
recipientPublicAccount,
45-
);
60+
public static decrypt(encryptMessage: EncryptedMessage, privateKey: string, recipientPublicAccount: PublicAccount): PlainMessage {
61+
return PlainMessage.create(Convert.hexToUtf8(Crypto.decode(privateKey, recipientPublicAccount.publicKey, encryptMessage.payload)));
4662
}
4763

4864
/**
@@ -54,17 +70,44 @@ export class EncryptedMessage extends Message {
5470
* @param payload
5571
*/
5672
public static createFromPayload(payload: string): EncryptedMessage {
57-
return new EncryptedMessage(this.decodeHex(payload));
73+
return new EncryptedMessage(EncryptedMessage.createBuffer(payload));
5874
}
5975

6076
/**
6177
*
62-
* @param encryptMessage - Encrypted message to be decrypted
63-
* @param privateKey - Recipient private key
64-
* @param recipientPublicAccount - Sender public account
65-
* @return {PlainMessage}
78+
* It creates the Plain message from a payload hex with the 00 prefix.
79+
*
80+
* @internal
81+
*/
82+
public static createFromBuilder(builder: Uint8Array): EncryptedMessage {
83+
return new EncryptedMessage(builder);
84+
}
85+
86+
/**
87+
* Create DTO object
6688
*/
67-
public static decrypt(encryptMessage: EncryptedMessage, privateKey, recipientPublicAccount: PublicAccount): PlainMessage {
68-
return new PlainMessage(this.decodeHex(Crypto.decode(privateKey, recipientPublicAccount.publicKey, encryptMessage.payload)));
89+
toDTO(): string {
90+
return Convert.uint8ToHex(this.toBuffer());
91+
}
92+
93+
toBuffer(): Uint8Array {
94+
return this.buffer;
95+
}
96+
97+
public static createBuffer(payload: string): Uint8Array {
98+
if (!payload) {
99+
return Uint8Array.of();
100+
}
101+
const message = Convert.utf8ToHex(payload);
102+
const payloadBuffer = Convert.hexToUint8(message);
103+
const typeBuffer = GeneratorUtils.uintToBuffer(MessageType.EncryptedMessage, 1);
104+
return GeneratorUtils.concatTypedArrays(typeBuffer, payloadBuffer);
105+
}
106+
107+
public static getPayload(buffer: Uint8Array): string {
108+
if (!buffer.length) {
109+
return '';
110+
}
111+
return Convert.uint8ToUtf8(buffer.slice(1));
69112
}
70113
}

Diff for: src/model/message/Message.ts

+12-34
Original file line numberDiff line numberDiff line change
@@ -15,51 +15,29 @@ import { Convert } from '../../core/format/Convert';
1515
* limitations under the License.
1616
*/
1717

18-
import { Convert } from '../../core/format';
1918
import { MessageType } from './MessageType';
2019

2120
/**
2221
* An abstract message class that serves as the base class of all message types.
2322
*/
24-
export abstract class Message {
23+
export interface Message {
2524
/**
26-
* @internal
27-
* @param hex
28-
* @returns {string}
25+
* The buffer to be used when serializing a transaction
2926
*/
30-
public static decodeHex(hex: string): string {
31-
return Buffer.from(hex, 'hex').toString();
32-
}
27+
toBuffer(): Uint8Array;
3328

3429
/**
35-
* @internal
36-
* @param type
37-
* @param payload
30+
* Create DTO object
3831
*/
39-
constructor(
40-
/**
41-
* Message type
42-
*/
43-
public readonly type: MessageType,
44-
/**
45-
* Message payload, it could be the message hex, encryped text or plain text depending on the message type.
46-
*/
47-
public readonly payload: string,
48-
) {}
32+
toDTO(): string;
4933

5034
/**
51-
* Create DTO object
35+
* validate if the content is correct
36+
*/
37+
readonly type: MessageType;
38+
39+
/**
40+
* Payload without type prefix.
5241
*/
53-
toDTO(): string {
54-
if (!this.payload) {
55-
return '';
56-
}
57-
if (this.type === MessageType.PersistentHarvestingDelegationMessage) {
58-
return this.payload;
59-
}
60-
if (this.type === MessageType.RawMessage) {
61-
return this.payload;
62-
}
63-
return this.type.toString(16).padStart(2, '0').toUpperCase() + Convert.utf8ToHex(this.payload);
64-
}
42+
readonly payload: string;
6543
}

Diff for: src/model/message/MessageFactory.ts

+14-13
Original file line numberDiff line numberDiff line change
@@ -34,31 +34,32 @@ export class MessageFactory {
3434
* @param payload the payload as byte array
3535
*/
3636
public static createMessageFromBuffer(payload?: Uint8Array): Message {
37-
return this.createMessageFromHex(payload ? Convert.uint8ToHex(payload) : undefined);
38-
}
39-
/**
40-
* It creates a message from the hex payload
41-
* @param payload the payload as hex
42-
*/
43-
public static createMessageFromHex(payload?: string): Message {
4437
if (!payload || !payload.length) {
45-
return new RawMessage('');
38+
return RawMessage.create(Uint8Array.of());
4639
}
47-
const upperCasePayload = payload.toUpperCase();
40+
const messageType = payload[0];
41+
const upperCasePayload = Convert.uint8ToHex(payload).toUpperCase();
4842
if (
4943
upperCasePayload.length == PersistentHarvestingDelegationMessage.HEX_PAYLOAD_SIZE &&
5044
upperCasePayload.startsWith(MessageMarker.PersistentDelegationUnlock)
5145
) {
5246
return PersistentHarvestingDelegationMessage.createFromPayload(upperCasePayload);
5347
}
54-
const messageType = Convert.hexToUint8(upperCasePayload)[0];
48+
5549
switch (messageType) {
5650
case MessageType.PlainMessage:
57-
return PlainMessage.createFromPayload(upperCasePayload.substring(2));
51+
return PlainMessage.createFromBuilder(payload);
5852
case MessageType.EncryptedMessage:
59-
return EncryptedMessage.createFromPayload(upperCasePayload.substring(2));
53+
return EncryptedMessage.createFromBuilder(payload);
6054
}
61-
return new RawMessage(upperCasePayload);
55+
return RawMessage.create(payload);
56+
}
57+
/**
58+
* It creates a message from the hex payload
59+
* @param payload the payload as hex
60+
*/
61+
public static createMessageFromHex(payload?: string): Message {
62+
return MessageFactory.createMessageFromBuffer(payload ? Convert.hexToUint8(payload) : undefined);
6263
}
6364
}
6465

Diff for: src/model/message/PersistentHarvestingDelegationMessage.ts

+18-3
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,15 @@ import { Message } from './Message';
2222
import { MessageMarker } from './MessageMarker';
2323
import { MessageType } from './MessageType';
2424

25-
export class PersistentHarvestingDelegationMessage extends Message {
25+
export class PersistentHarvestingDelegationMessage implements Message {
26+
public type = MessageType.PersistentHarvestingDelegationMessage;
2627
public static readonly HEX_PAYLOAD_SIZE = 264;
2728

28-
constructor(payload: string) {
29-
super(MessageType.PersistentHarvestingDelegationMessage, payload.toUpperCase());
29+
/**
30+
* @internal
31+
* @param payload
32+
*/
33+
constructor(public readonly payload: string) {
3034
if (!Convert.isHexString(payload)) {
3135
throw Error('Payload format is not valid hexadecimal string');
3236
}
@@ -86,4 +90,15 @@ export class PersistentHarvestingDelegationMessage extends Message {
8690
const decrypted = Crypto.decode(privateKey, ephemeralPublicKey, payload);
8791
return decrypted.toUpperCase();
8892
}
93+
94+
/**
95+
* Create DTO object
96+
*/
97+
toDTO(): string {
98+
return this.payload;
99+
}
100+
101+
toBuffer(): Uint8Array {
102+
return Convert.hexToUint8(this.payload);
103+
}
89104
}

0 commit comments

Comments
 (0)