Integrating Encryption in the ComingChat App

In our social platform, we provide encrypted chat and a means of securely sending digital assets leveraging the Signal encryption protocol.

Integrating Encryption in the ComingChat App

ComingChat, a decentralized social platform built on the Sui network, includes encrypted chat among its many features, offering users a secure means of communicating. The feature leverages Signal's encryption protocol, open source software popular in such apps as Signal, WhatsApp, and Skype.

ComingChat provides a comprehensive lifestyle experience on Sui. It combines productivity enhanced by ChatGPT, social interactions, and an omni-chain wallet to offer users a wide range of features and applications.

Beyond its trustworthy and proven nature, we chose the Signal protocol for ComingChat's encrypted communication because it is fully compatible with the Sui network's account system.

Chat Modes

ComingChat's chat module offers three different modes for users to communicate with one another, catering to their varying privacy and security needs. These modes are:

  • Private chat: This mode provides one-on-one communication between two users. The Signal protocol encrypts these conversations, keeping them confidential.
  • Encrypted group chat: This mode lets groups communicate while maintaining a high level of security and privacy. Similar to private chat, the Signal protocol encrypts messages in encrypted group chats.
  • Non-encrypted group chat: In this mode, messages are not encrypted but the group supports up to 1,000 people, making it easy to set up and manage but offering a lower level of protection for message content.

The Signal protocol implementation occurs within our chat infrastructure, which uses our Decentralized Moments (Dmens) protocol. This protocol supports common chat features such as posting, liking, and replying. We also integrate a Red Envelope feature, which lets users send tokens or NFTs to each other.

Implementing Encryption

ComingChat's Encrypted Chat feature is based on the Signal Protocol's Double Ratchet Algorithm, which provides end-to-end encryption and ensures secure communication between users.

Building the Encrypted Chat feature required the following steps:

  1. Implement the Double Ratchet Algorithm to enable end-to-end encryption for messages, ensuring that only the intended recipients can decrypt and read them.
  2. Store encrypted messages on the Sui network to ensure data integrity and security.
  3. Allow users to exchange public keys for secure communication and establish secure sessions using the Extended Triple Elliptic-curve Diffie-Hellman (X3DH) key agreement protocol.
diagram of ComingChat's chat architecture
ComingChat's encrypted chat architecture lets a user select a private chat, after which it will encrypt messages sent to another user, then decrypting those messages so the other user can read them.

The Double Ratchet Algorithm

The Double Ratchet Algorithm is used by two parties to exchange encrypted messages based on a shared secret key. Typically, the parties will use a key agreement protocol, such as X3DH, to agree on the shared secret key. Following this agreement, the parties will use the Double Ratchet to send and receive encrypted messages.

The parties derive new keys for every Double Ratchet message so that earlier keys cannot be calculated from later ones. The parties also send Diffie-Hellman public values attached to their messages. The results of Diffie-Hellman calculations are mixed into the derived keys so that later keys cannot be calculated from earlier ones. These properties give some protection to earlier or later encrypted messages in case of a compromise of a party's keys.

Privacy-Enhanced Red Envelopes

The term Red Envelope for a secure online transaction originates from the practice of giving money to others in a physical red envelope during Chinese holidays and special occasions. We use the term in ComingChat for the secure message packets in our encrypted chat feature.

Sui's Move language variant allows some unique differences in how we developed our red envelope contract versus Core Move. In particular, Move requires a variation on programming the transaction status synchronization return, which helps order the chat messages, and the entry function parameters, which need an object ID.

  • Sui red packets do not have an incremental redPacketId, but they do have a redPacket ObjectId, because Sui's data model requires that all objects have an ID.
  • Sui red packets do not need to asynchronously obtain the status after the open / close transaction is issued; the red envelope status can be updated according to the event in the transaction return data.
  • The server-side node needs to asynchronously obtain the creation status of a red packet created by a user, as that user's node may differ from that of the server-side node.

The core contract code below shows how we took into account the unique capabilities and requirements of Move on Sui. The Config object contains the addresses for the sender, receiver, and administrator, while also defining the transaction fees. The RedPacketInfo object includes a coin balance, a token being sent, and the receiver's address. The RedPacketEvent object keeps track of the token balance.

// Copyright 2022-2023 ComingChat Authors. Licensed under Apache-2.0 License.
module rp::red_packet {
	…
	struct Config has key {
	id: UID, 
	admin: address, 
	beneficiary: address, 
	owner: address, 
	count: u64, 
	fees: Bag
}

struct RedPacketInfo<phantom CoinType> has key,store {
	id: UID, 
	remain_coin: Balance<CoinType>, 
	remain count: u64, 
	beneficiary: address
}

// Event emitted when created/opened/closed a red packet.
struct RedPacketEvent has copy, drop {
	id: ID, 
	event_type: u8, 
	remain_count: u64, 
	remain balance: u64

// One-Time-Witness for the module.
struct RED_PACKET has drop {}

fun init (
	otw: RED_PACKET, 
	ct: &mut TxContext
) {
		…
}

public entry fun create<CoinType> (
	config: &mut Config,
	coins: vector<Coin<CoinType>>, 
	count: u64, 
	total_balance: u64, 
	ctx: &mut TxContext
) {
	// 1. check args
	…

}

public entry fun open<CoinType> (
	info: &mut RedPacketInfo<CoinType>, 
	lucky_accounts: vector<address>, 
	balances: vector<u64>, 
	ct: &mut TXContext
) {
…
}

public entry fun close<CoinType> (
	info: RedPacketInfo CoinType>, 
	ctx: &mut TxContext
) {
…
}

public entry fun withdraw<CoinType> (
	config: Smut Config, 
	ctx: &mut TxContext
) {
…
}

When submitting the Sui red packet open / close transaction, the transaction result is directly obtained in the response, and the database and cache status are updated, without the need to asynchronously obtain the transaction status from the browser.

After the user creates the red envelope, the system asynchronously queries the create transaction status and obtains the red envelope data, including the amount, quantity, and red envelope ID based on the event.

diagrom of ComingChat back end architecture
In our app architecture, ComingChat sends the Create RedEnvelope event to the Sui network as a smart contract. We process the status of that contract based on the receiver's action.

Red Packet Status Change

After ComingChat sends the Sui transaction, it obtains the transaction result directly, and there is no need for asynchronous tasks to update the open / close status, so:

  • After triggering the open condition, the admin calls the open transaction and sets the record directly to success according to the open transaction status.
  • After triggering the close condition, the admin calls the close transaction and sets it directly to closed or close fail according to the close transaction status.

If the open / close transaction fails, the failure needs to be recorded to prevent transaction retries, which will incur extraneous gas fees.

diagram showing flow of redenvelope transactions
We monitor the transaction state to either close a successful transaction, or acknowledge a failed state and stop auto-retries to avoid unnecessary gas fees.

Dmens Protocol

We built the Decentralized Moments (Dmens) protocol as an SDK on Sui to provide functions such as user identification, content sharing, and value sharing. This protocol uses Sui to manage user data and content, and pays gas fees with the SUI token. Users can create profiles, post content, follow other users, and interact with them. The protocol also lets users convert their created content into unique and non-fungible tokens (NFTs) and issue different types of NFTs for various scenarios.

These scenarios include:

  1. Key opinion leaders (KOLs) issue valuable NFTs to their fans to increase fan engagement, loyalty, and revenue.
  2. Projects issue equity proof NFTs for operational activities to increase user participation, loyalty, and promote ecosystem development.
  3. Content creators monetize their content through a paid NFT model, achieving better content monetization and more revenue.
  4. Artists convert their digital artworks into NFTs and sell them to collectors or investors.

Dmens Architecture

We architected Dmens in ComingChat to support our public and private chat features. As an overview, when a user creates a message, which could be new or a reply, it initiates the chat function in ComingChat on Sui. We use GraphQL to query off-chain storage for user profiles and a Dmens indexer module to ensure messages are properly ordered.

diagram of flow between sui and dmens indexer
The Dmens architecture uses Sui, GraphQL, and the Dmens indexer to process user actions, such as creating a profile or posting a new message. Here, GraphQL serves as a database query for stored profiles.

In the smart contract code below, we define the Chat object, which will let users post a message, repost another message, like a message, and other typical chat functions.

//chat.move
module chat::chat {
	/// Sui Chat NFT (i.e., a post, retweet, like, chat message etc).
	struct Chat has key, store {
		id: UID,
		// The ID of the chat app.
		app_id: address,
		// Post's text.
		text: String,
		// Set if referencing an another object (i.e., due to a Like, Retweet, Reply etc).
		// We allow referencing any object type, not only Chat NFTs.
		ref id: Option<address>,
		// app-specific metadata. We do not enforce a metadata format and delegate this to app layer.
		metadata: vector<u8>,
	}

	/// Simple Chat.text getter.
	public fun text (chat: &Chat): String {
		chat.text
	}

	/// Mint (post) a Chat object.
	fun post internal (
		app_id: address, 
		text: vector<u8>, 
		ref_id: Option<address>, 
		metadata: vector<u8>, 
		ctx: &mut TxContext,
	) {
		…
	}


	/// Mint (post) a Chat object without referencing another object.
	public entry fun post (
		app_identifier: address, 
		text: vector<u8>, 
		metadata: vector<u8>, 
		ctx: &mut IxContext,
	) {
		post_internal(app_identifier, text, option::none (), metadata, ctx);
	}

	/// Mint (post) a Chat object and reference another object (i.e., to simulate retweet, reply, like, attach).
	/// TODO: Using address as app_identifier & 'ref_identifier type, because we cannot pass 'ID' to entry functions. Using vector<u8>' for text instead of String' for the same reason.
	public entry fun post_with_ref 
		app_identifier: address, 
		text: vector<u8>, 
		ref_identifier: address, 
		metadata: vector<u8>, 
		ctx: &mut TxContext,
	) {
		post_internal(app_identifier, text, some (ref_identifier), metadata, ctx);
	}

	/// Burn a Chat object.
	public entry fun burn (chat: Chat) {
		let Chat { id, app_id: _, text: _ , ref_id: _, metadata: _ } = chat;
		object::delete (id);
	}
}

The Chat struct in the above code snippet represents a chat message. It has ID fields, including ref_id, which lets the chat message retweet, reply, or like another message, which would appear as an object in the code. The actual chat message is the text string in the struct.

The post internal function creates a new chat message. It's marked as "internal" because it is meant to be called from inside the module. The objects created by this function have ID fields and a text string for the actual message. The ref_id lets it refer to another object, as a reply or a like to an existing chat.

Similarly, we have the post public entry function, which calls post internal to create a new chat. However, it sets ref_id to none as this function is intended for people to initiate new chats.

Dmens Indexer Structural Design

ComingChat's encrypted chat module uses Redis, an open source streaming database, as off-chain storage. It handles the message queue, ensuring chat messages appear in an ordered manner.

For our Redis stream, we begin by initializing the customer.

func (r *BaseRedisCustomer) InitCustomer ( ) error {
	...
}

Redis stores data in RAM, so the queue data needs to be pruned appropriately and regularly. In the code snippet below, we define a function to trim the queue.

func (r *BaseRedisCustomer) TrimQueueList (ct context.Context) {
	r.wg.Add (1)
	defer funct( ) { 
		r.wg.Done ( )
	} ( )
	for {
		select {
		case <-ctx.Done ( ) :
			return
	}
	…
}

The listener code filters transactions by contract address. The function in the code below is a good example of how to integrate traditional off-chain storage with a Web3 app.

func (1 *ListenLastIxByCycle) cycleFetchTransactionNum(ct context. Context, tx chan<-TxDigest) {
	var (
		cursor *types.TransactionDigest
	)
	…
}

The code snippet below pushes each new transaction digest to a queue called transaction-analyze.

rpip.Evalsha (
	r.Context (), 
	r.script["pushNewT×DigestToStream" ],
	[ ]string{topic, fmt.Sprintf(PrefixChainLastDigest, chain, packageId) },
	"data", 
	preDigest, 
	digest,

	)

We use Lua server-side scripting to combine multiple Redis commands, ensuring transaction digest continuity.

local lastDigest = redis.call( 'get', KEYS[2])
local result = false
if (lastDigest ~= false) and (lastDigest == ARG[2])) or ((lastDigest == false) and (ARGVI 21 == ')) then
	redis.call('xadd', KEYS[1], '*', ARGV[1], ARGV[3]) 
	redis.call ('set', KEYS[2], ARGV[3])
end
return true

The indexer goes through the following processes as it receives each message submitted by users:

Cron Job

  1. Query failure message of all consumers from table queue_message.
  2. Re-consume according to the topic. If a job exceeds the threshold for re-consumption, it must be stopped and accessed manually.

Consumer

  • Analyze transaction
    1. Query object which was impacted by this transaction and excluding the coin object
    2. Push impacted object to queue
  • Object update
    1. Fetch object detail and create or update it on the object_list table
    2. Filter profile object
    3. Filter tweets which call the ChatGPT
  • Analyze profile
    1. decode profile object
  • GPT reply
    1. Get Dmens tweet content and use regex to match the gpt bot address

Conclusion

Encrypted chat has proven itself very popular in apps such as Signal, WhatsApp, and WeChat. Including it in ComingChat fit well with our existing ideas for a social platform on Sui. Encryption gives users privacy, ensuring that bad actors won't be able to eavesdrop on their conversations. Encrypted chat also dovetails with the features of Sui, giving users an independent and secure platform to conduct their online lives.

Our technical implementation leveraged the trusted Signal Protocol's Double Ratchet Algorithm, showing how existing technologies can be adapted for Web3 platforms. Our inclusion of advanced features, shown above with Dmens, Red Packets, and Chatbots, allows for a rich user experience on ComingChat.