Skip to content

Commit 191f4d0

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 191f4d0

17 files changed

+318
-159
lines changed

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

src/core/crypto/Crypto.ts

+16-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,8 @@ export class Crypto {
171177
try {
172178
const decoded = Crypto._decode(recipientPrivate, senderPublic, payloadBuffer, tagAndIv);
173179
return decoded.toUpperCase();
174-
} catch {
180+
} catch (e) {
181+
console.log(e);
175182
// To return empty string rather than error throwing if authentication failed
176183
return '';
177184
}
@@ -183,7 +190,7 @@ export class Crypto {
183190
*
184191
* @return {Uint8Array}
185192
*/
186-
public static randomBytes = (length: number): any => {
193+
public static randomBytes = (length: number): Buffer => {
187194
return crypto.randomBytes(length);
188195
};
189196
}

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
}

src/model/message/EncryptedMessage.ts

+63-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,42 @@ 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;
31+
public readonly buffer: Uint8Array;
2832

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

3442
/**
3543
*
3644
* @param message - Plain message to be encrypted
3745
* @param recipientPublicAccount - Recipient public account
3846
* @param privateKey - Sender private key
39-
* @return {EncryptedMessage}
47+
* @param iv - iv for encoding, for unit tests.
48+
* @return The encrypted message.
49+
*/
50+
public static create(message: string, recipientPublicAccount: PublicAccount, privateKey: string, iv?: Buffer): EncryptedMessage {
51+
const encryptedHex = Crypto.encode(privateKey, recipientPublicAccount.publicKey, message, false, iv).toUpperCase();
52+
return new EncryptedMessage(EncryptedMessage.createBuffer(encryptedHex));
53+
}
54+
55+
/**
56+
*
57+
* @param encryptMessage - Encrypted message to be decrypted
58+
* @param privateKey - Recipient private key
59+
* @param recipientPublicAccount - Sender public account
60+
* @return {PlainMessage}
4061
*/
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-
);
62+
public static decrypt(encryptMessage: EncryptedMessage, privateKey: string, recipientPublicAccount: PublicAccount): PlainMessage {
63+
return PlainMessage.create(Convert.hexToUtf8(Crypto.decode(privateKey, recipientPublicAccount.publicKey, encryptMessage.payload)));
4664
}
4765

4866
/**
@@ -54,17 +72,44 @@ export class EncryptedMessage extends Message {
5472
* @param payload
5573
*/
5674
public static createFromPayload(payload: string): EncryptedMessage {
57-
return new EncryptedMessage(this.decodeHex(payload));
75+
return new EncryptedMessage(EncryptedMessage.createBuffer(payload));
5876
}
5977

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

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
}

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

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)