Skip to content

fix: improved message api #818

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.

The changelog format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [1.0.4] - NEXY
## [1.0.4] - NEXT

**Milestone**: Symbol Mainnet
Package | Version | Link
Expand All @@ -15,6 +15,8 @@ Client Library | v1.0.3 | [symbol-openapi-typescript-fetch-client](https://www.

- fix: Upgraded Node to 12.22.1.
- fix: Upgraded typescript to 4.5.4.
- fix: Improved message API.
- fix: EncryptedMessage payload wasn't reproducible.
- fix: Upgraded RXJS to 7.4.0.

## [1.0.3] - 16-Nov-2021
Expand Down
27 changes: 16 additions & 11 deletions src/core/crypto/Crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,19 +107,25 @@ export class Crypto {
/**
* Encode a message using AES-GCM algorithm
*
* @param {string} senderPriv - A sender private key
* @param {string} recipientPub - A recipient public key
* @param {string} msg - A text message
* @param {boolean} isHexString - Is payload string a hexadecimal string (default = false)
* @param senderPriv - A sender private key
* @param recipientPub - A recipient public key
* @param msg - A text message
* @param isHexString - Is payload string a hexadecimal string (default = false)
* @param iv - the iv for unit testing, otherwise a random 12 byte array.
*
* @return {string} - The encoded message
*/
public static encode = (senderPriv: string, recipientPub: string, msg: string, isHexString = false): string => {
public static encode = (
senderPriv: string,
recipientPub: string,
msg: string,
isHexString = false,
iv = Crypto.randomBytes(12),
): string => {
// Errors
if (!senderPriv || !recipientPub || !msg) {
throw new Error('Missing argument !');
}
// Processing
const iv = Crypto.randomBytes(12);
const encoded = Crypto._encode(senderPriv, recipientPub, isHexString ? msg : convert.utf8ToHex(msg), iv);
// Result
return encoded;
Expand Down Expand Up @@ -171,9 +177,8 @@ export class Crypto {
try {
const decoded = Crypto._decode(recipientPrivate, senderPublic, payloadBuffer, tagAndIv);
return decoded.toUpperCase();
} catch {
// To return empty string rather than error throwing if authentication failed
return '';
} catch (e) {
throw new Error(`Cannot decrypt payload. Error: ${e.message}`);
}
};

Expand All @@ -183,7 +188,7 @@ export class Crypto {
*
* @return {Uint8Array}
*/
public static randomBytes = (length: number): any => {
public static randomBytes = (length: number): Buffer => {
return crypto.randomBytes(length);
};
}
34 changes: 33 additions & 1 deletion src/core/format/Convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,26 @@ export class Convert {
* @return {string}
*/
public static utf8ToHex = (input: string): string => {
return Buffer.from(input, 'utf-8').toString('hex').toUpperCase();
return Convert.utf8ToBuffer(input).toString('hex').toUpperCase();
};

/**
* Convert hex to UTF-8
* @param {string} hex - an hex string
* @return {string} An UTF-8 string
*/
public static hexToUtf8 = (hex: string): string => {
return Buffer.from(hex, 'hex').toString();
};
/**
* Convert UTF-8 to buffer
* @param input - An UTF-8 string
* @return the buffer
*/
public static utf8ToBuffer(input: string): Buffer {
return Buffer.from(input, 'utf-8');
}

/**
* Convert UTF-8 string to Uint8Array
* @param {string} input - An string with UTF-8 encoding
Expand Down Expand Up @@ -266,4 +283,19 @@ export class Convert {
}
return value >>> 0;
}
/**
* It concats a list of Uint8Array into a new one.
*
* @param arrays - the Uint8Array to concat.
*/
public static concat(...arrays: Uint8Array[]): Uint8Array {
const totalLength = arrays.reduce((acc, value) => acc + value.length, 0);
const result = new Uint8Array(totalLength);
let length = 0;
for (const array of arrays) {
result.set(array, length);
length += array.length;
}
return result;
}
}
79 changes: 61 additions & 18 deletions src/model/message/EncryptedMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
* limitations under the License.
*/

import { GeneratorUtils } from 'catbuffer-typescript';
import { Convert } from '../../core';
import { Crypto } from '../../core/crypto';
import { PublicAccount } from '../account';
import { Message } from './Message';
Expand All @@ -23,26 +25,40 @@ import { PlainMessage } from './PlainMessage';
/**
* Encrypted Message model
*/
export class EncryptedMessage extends Message {
public readonly recipientPublicAccount?: PublicAccount;
export class EncryptedMessage implements Message {
public readonly type = MessageType.EncryptedMessage;
public readonly payload: string;

constructor(payload: string, recipientPublicAccount?: PublicAccount) {
super(MessageType.EncryptedMessage, payload);
this.recipientPublicAccount = recipientPublicAccount;
/**
* @internal
* @param buffer the buffer.
*/
constructor(private readonly buffer: Uint8Array) {
this.payload = EncryptedMessage.getPayload(buffer);
}

/**
*
* @param message - Plain message to be encrypted
* @param recipientPublicAccount - Recipient public account
* @param privateKey - Sender private key
* @return {EncryptedMessage}
* @param iv - iv for encoding, for unit tests.
* @return The encrypted message.
*/
public static create(message: string, recipientPublicAccount: PublicAccount, privateKey: string, iv?: Buffer): EncryptedMessage {
const encryptedHex = Crypto.encode(privateKey, recipientPublicAccount.publicKey, message, false, iv).toUpperCase();
return new EncryptedMessage(EncryptedMessage.createBuffer(encryptedHex));
}

/**
*
* @param encryptMessage - Encrypted message to be decrypted
* @param privateKey - Recipient private key
* @param recipientPublicAccount - Sender public account
* @return {PlainMessage}
*/
public static create(message: string, recipientPublicAccount: PublicAccount, privateKey: string): EncryptedMessage {
return new EncryptedMessage(
Crypto.encode(privateKey, recipientPublicAccount.publicKey, message).toUpperCase(),
recipientPublicAccount,
);
public static decrypt(encryptMessage: EncryptedMessage, privateKey: string, recipientPublicAccount: PublicAccount): PlainMessage {
return PlainMessage.create(Convert.hexToUtf8(Crypto.decode(privateKey, recipientPublicAccount.publicKey, encryptMessage.payload)));
}

/**
Expand All @@ -54,17 +70,44 @@ export class EncryptedMessage extends Message {
* @param payload
*/
public static createFromPayload(payload: string): EncryptedMessage {
return new EncryptedMessage(this.decodeHex(payload));
return new EncryptedMessage(EncryptedMessage.createBuffer(payload));
}

/**
*
* @param encryptMessage - Encrypted message to be decrypted
* @param privateKey - Recipient private key
* @param recipientPublicAccount - Sender public account
* @return {PlainMessage}
* It creates the Plain message from a payload hex with the 00 prefix.
*
* @internal
*/
public static createFromBuilder(builder: Uint8Array): EncryptedMessage {
return new EncryptedMessage(builder);
}

/**
* Create DTO object
*/
public static decrypt(encryptMessage: EncryptedMessage, privateKey, recipientPublicAccount: PublicAccount): PlainMessage {
return new PlainMessage(this.decodeHex(Crypto.decode(privateKey, recipientPublicAccount.publicKey, encryptMessage.payload)));
toDTO(): string {
return Convert.uint8ToHex(this.toBuffer());
}

toBuffer(): Uint8Array {
return this.buffer;
}

public static createBuffer(payload: string): Uint8Array {
if (!payload) {
return Uint8Array.of();
}
const message = Convert.utf8ToHex(payload);
const payloadBuffer = Convert.hexToUint8(message);
const typeBuffer = GeneratorUtils.uintToBuffer(MessageType.EncryptedMessage, 1);
return GeneratorUtils.concatTypedArrays(typeBuffer, payloadBuffer);
}

public static getPayload(buffer: Uint8Array): string {
if (!buffer.length) {
return '';
}
return Convert.uint8ToUtf8(buffer.slice(1));
}
}
46 changes: 12 additions & 34 deletions src/model/message/Message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,51 +15,29 @@ import { Convert } from '../../core/format/Convert';
* limitations under the License.
*/

import { Convert } from '../../core/format';
import { MessageType } from './MessageType';

/**
* An abstract message class that serves as the base class of all message types.
*/
export abstract class Message {
export interface Message {
/**
* @internal
* @param hex
* @returns {string}
* The buffer to be used when serializing a transaction
*/
public static decodeHex(hex: string): string {
return Buffer.from(hex, 'hex').toString();
}
toBuffer(): Uint8Array;

/**
* @internal
* @param type
* @param payload
* Create DTO object
*/
constructor(
/**
* Message type
*/
public readonly type: MessageType,
/**
* Message payload, it could be the message hex, encryped text or plain text depending on the message type.
*/
public readonly payload: string,
) {}
toDTO(): string;

/**
* Create DTO object
* validate if the content is correct
*/
readonly type: MessageType;

/**
* Payload without type prefix.
*/
toDTO(): string {
if (!this.payload) {
return '';
}
if (this.type === MessageType.PersistentHarvestingDelegationMessage) {
return this.payload;
}
if (this.type === MessageType.RawMessage) {
return this.payload;
}
return this.type.toString(16).padStart(2, '0').toUpperCase() + Convert.utf8ToHex(this.payload);
}
readonly payload: string;
}
27 changes: 14 additions & 13 deletions src/model/message/MessageFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,31 +34,32 @@ export class MessageFactory {
* @param payload the payload as byte array
*/
public static createMessageFromBuffer(payload?: Uint8Array): Message {
return this.createMessageFromHex(payload ? Convert.uint8ToHex(payload) : undefined);
}
/**
* It creates a message from the hex payload
* @param payload the payload as hex
*/
public static createMessageFromHex(payload?: string): Message {
if (!payload || !payload.length) {
return new RawMessage('');
return RawMessage.create(Uint8Array.of());
}
const upperCasePayload = payload.toUpperCase();
const messageType = payload[0];
const upperCasePayload = Convert.uint8ToHex(payload).toUpperCase();
if (
upperCasePayload.length == PersistentHarvestingDelegationMessage.HEX_PAYLOAD_SIZE &&
upperCasePayload.startsWith(MessageMarker.PersistentDelegationUnlock)
) {
return PersistentHarvestingDelegationMessage.createFromPayload(upperCasePayload);
}
const messageType = Convert.hexToUint8(upperCasePayload)[0];

switch (messageType) {
case MessageType.PlainMessage:
return PlainMessage.createFromPayload(upperCasePayload.substring(2));
return PlainMessage.createFromBuilder(payload);
case MessageType.EncryptedMessage:
return EncryptedMessage.createFromPayload(upperCasePayload.substring(2));
return EncryptedMessage.createFromBuilder(payload);
}
return new RawMessage(upperCasePayload);
return RawMessage.create(payload);
}
/**
* It creates a message from the hex payload
* @param payload the payload as hex
*/
public static createMessageFromHex(payload?: string): Message {
return MessageFactory.createMessageFromBuffer(payload ? Convert.hexToUint8(payload) : undefined);
}
}

Expand Down
21 changes: 18 additions & 3 deletions src/model/message/PersistentHarvestingDelegationMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ import { Message } from './Message';
import { MessageMarker } from './MessageMarker';
import { MessageType } from './MessageType';

export class PersistentHarvestingDelegationMessage extends Message {
export class PersistentHarvestingDelegationMessage implements Message {
public type = MessageType.PersistentHarvestingDelegationMessage;
public static readonly HEX_PAYLOAD_SIZE = 264;

constructor(payload: string) {
super(MessageType.PersistentHarvestingDelegationMessage, payload.toUpperCase());
/**
* @internal
* @param payload
*/
constructor(public readonly payload: string) {
if (!Convert.isHexString(payload)) {
throw Error('Payload format is not valid hexadecimal string');
}
Expand Down Expand Up @@ -86,4 +90,15 @@ export class PersistentHarvestingDelegationMessage extends Message {
const decrypted = Crypto.decode(privateKey, ephemeralPublicKey, payload);
return decrypted.toUpperCase();
}

/**
* Create DTO object
*/
toDTO(): string {
return this.payload;
}

toBuffer(): Uint8Array {
return Convert.hexToUint8(this.payload);
}
}
Loading