Skip to main content

Creating an encrypted desktop chat in Rust

Summary: #

How hard is it to create an encrypted chat application? In this article we will create a Desktop Rust app that will use the networking routing protocol Veilid and the UI framework iced.


Intro #

There are thousands of chat applications and instant messagenger applications that market themself as encrypted chats, like Signal, Potato chat, Session, etc. The list is long. How hard is it to build a chat application in Rust in a weekend? This we will find out.

Design goal: #

  • Messaging/Network layer
    Allowing users to communicate without seeing each other’s IP addresses and sending messages in an encrypted way.
  • Peer-to-peer, Resilent towards centralisation The app should not fail if one centralised datacenter or entity gets shut down, it will always be be able to operate like Zeronet and/or Bittorrent.
  • Desktop User Interface A fresh and easy-to-use Desktop interface that we can distribute with flatpak via flathub.org.

Select platform: #

As a smaller project we want to aim at targeting desktop users.

Networking #

We want to give Veilid a go for the networking layer, as it allows us to construct private routes, in a true p2p way directly from user to user.
Using a smaller experimental project such as Veilid makes it more fun and it also ships with tokio support, making it easy to work with.

Ideally we want to try to make a system that is hard to censor, an attack should not be able to detect and block our traffic, a good reference point to this is snowflake from Tor.

Desktop User Interface #

We want something fresh, well-documented and Desktop native. We went with Iced.rs

https://iced.rs/

Iced is Rust UI library with a clean design and lots of features packed into it.

Iced Philosophy https://book.iced.rs/philosophy.html

Some nice Desktop app developers have created with Iced: #


Pokemon’s <3

But the one that was most eye-catching, for me as an former IRC addict
is Halloy Chat:
An open source IRC client that comes with Tor(arti) support out of the box
Halloy chat, is also very nicely documented, including a flatpak guide: https://Halloy.chat/guides/flatpaks.html

This we can use to easily upload our Rust desktop app on flathub.org and distribute it to Linux users all around the globe.

Adding Veilid and not only IRC #

Our plan is clear: Add support for user-to-user messages via Veilid’s network.

Connect to Veilid with Rust: #

Veilid comes with some code examples we can use to understand how to send: https://gitlab.com/veilid/veilid/-/tree/main/veilid-core/examples/private_route

let connect = "ARAeUAECAQJRBAEBURgBAg8wRExWEAT/7sdvWj8/v1kDYXE+XfS/ULf8x31NITI+Sdjf66qhzlTBAAARBARBEAL/JVPBhu0kDCADmZ0m5USL70BUlzXIyWMvlkwvYhSUwwWHEQQDMQ3yAf9w0MAGY9qk4gnK1d4qtaaKLcFTETRXD7N27+N4ZB46mcJ9csfX/uxApBr5BiDV/J0IU3u/CS1+k5MjmdMaiMvWdNqRh8RvBLE/3E60qWAAZMQ/JkdnKkeM";
    let blob: Vec<u8> = data_encoding::BASE64.decode(connect.as_bytes())?l;
    let _ = open_route(blob, config, String::from("yelllooow")).await;

How to Veilid: #

  • When we want to communicate, we will spawn a veilid node and get a base64 route ID, which we will give to someone who wants to communicate with us.

Start node:

pub async fn create_route(
    mut done_recv: tokio::sync::mpsc::Receiver<()>,
    mut config: VeilidConfig,
) -> Result<(), Box<dyn std::error::Error>> {
    // Use a namespace for the receiving side of the private route
    config.namespace = "recv".to_owned();

// Run veilid node
veilid_api_scope(update_callback, config, |veilid_api| async move {
        // Create a new private route endpoint
        let (route_id, route_blob) =
            try_again_loop(|| async { veilid_api.new_private_route().await }).await?;

        // Print the blob
println!("Route id created: {route_id}\nConnect with this private route blob:\ncargo run --example private-route-example -- --connect {}",
            data_encoding::BASE64.encode(&route_blob)
);

When the user starts the application, it should start a Veilid node and we just need to save the route blob: data_encoding::BASE64.encode(&route_blob) to disk so its easy to access, just like when you spin up a Tor hidden service and its saves the ‘.onion’ domain, it must be easy for users so lets store it as /etc/veilid/my_route or similar so the user can easily access this and send it to .
Routes are always to be considerd as temporary and a better implementation would be just put a copy route button in the UI.

Debug tips:
When you start a veilid node, it use a default directory, in order to spawn multiple veilid nodes fast, use a temporary VeilidConfig directory for debugging:

use std::path::PathBuf;                                      
use tempfile::tempdir;                                       
                                                             
fn get_temp_dir() -> Result<PathBuf, MyErrorClass> {              
    let temp_dir = tempdir()?;     
    let dir_path: PathBuf = temp_dir.into_path();            
    Ok(dir_path)                                             
}           

So we will:

  • Spawn a veilid node every time our app starts
  • Let users create user 2 user chats with Veilid format

Modify config: #

Right now we have take Halloy repo and extend it, lets modify the config to get not only irc networks but also Veilid to veilid chats:

# Halloy config.
#
# For a complete list of available options,
# please visit https://Halloy.chat/configuration.html

[servers.liberachat]
nickname = "__NICKNAME__"
server = "irc.libera.chat"
channels = ["#halloy"]
protocol = "irc"  # Optional: "irc" (default) or "veilid"

# Example veilid server configuration:
# [servers.veilid-example]
# protocol = "veilid"
# nickname = "__NICKNAME__"
# # For Veilid, you need to provide the route blob (base64 encoded) instead of server/port
# # Get this from the person you want to connect to (they need to spawn a veilid node first)
# veilid_route_blob = "ARAeUAECAQJRBAEBURgBAg8wRExWEAT/7sdvWj8/v1kDYXE+XfS/ULf8x31NITI+Sdjf66qhzlTBAAARBARBEAL/JVPBhu0kDCADmZ0m5USL70BUlzXIyWMvlkwvYhSUwwWHEQQDMQ3yAf9w0MAGY9qk4gnK1d4qtaaKLcFTETRXD7N27+N4ZB46mcJ9csfX/uxApBr5BiDV/J0IU3u/CS1+k5MjmdMaiMvWdNqRh8RvBLE/3E60qWAAZMQ/JkdnKkeM"

[servers.veilid-example]
protocol = "veilid"
nickname = "VeilidTest"
# # For Veilid, you need to provide the route blob (base64 encoded) instead of server/port
# # Get this from the person you want to connect to (they need to spawn a veilid node first)
veilid_route_blob = "ARAeUAECAQJRBAEBURgBAg8wRExWEAT/7sdvWj8/v1kDYXE+XfS/ULf8x31NITI+Sdjf66qhzlTBAAARBARBEAL/JVPBhu0kDCADmZ0m5USL70BUlzXIyWMvlkwvYhSUwwWHEQQDMQ3yAf9w0MAGY9qk4gnK1d4qtaaKLcFTETRXD7N27+N4ZB46mcJ9csfX/uxApBr5BiDV/J0IU3u/CS1+k5MjmdMaiMvWdNqRh8RvBLE/3E60qWAAZMQ/JkdnKkeM"

Let’s modify the template config handler and we can load veilid chats.

Now all we need to do is get our friends to write to us via Veilid! In order to make it easier, we should setup a website that maps Veilid route blobs to something smaller and easier to copy. Maybe encode and compress the string more or alternatively just make a simple link forwarder style website where users can enter their Veilid blob and share a link to other users easily.

So what do we have now? #

  • Basic PoC app with Veilid connections
    User starts chat client, copies the Veilid route and gives it to another user.

So we can now route messages from one user to another via Veilid, but so far if user wants group chats, you need to roll up the sleeves and implement it yourself, alternively find a way to wrap IRC group chat logic into a Veilid <> Veilid connection flow.

Add a layer of Post Quantum to your chat: #

As the time of writing this, most Veilid nodes are still running an early version VLD0 and the talented developers behind Veilid are actively working towards shipping Post Quantum support of the box, so in the near future this might not be needed to add extra lego blocks.

Sense this is a quick project, we are going to use an un-audited Rust crate(This is scary, dont do this in production) of the post Quantom algorithm Kyber.

We want to wrap messages sent via Veilid in a Quantum Safe encrypted blanket ;3

Let’s use:
https://crates.io/crates/pqc_kyber https://github.com/RustCrypto/KEMs/tree/master/ml-kem

use pqc_kyber::{keypair, encapsulate, KYBER_CIPHERTEXT_BYTES, PUBLICKEYBYTES};
use aes_gcm::{Aes256Gcm, KeyInit, aead::{Aead, OsRng, generic_array::GenericArray}};
use rand_core::RngCore;

pub async fn send_message(
    message: &str,
    recipient: &UserIdentity,
    sender_identity: &UserIdentity,
    route_id: veilid_core::routing_table::RouteId,
    network_context: &Arc<veilid_core::NetworkContext>,
) -> Result<[u8; 16], CryptoError> {
    log::info!("Sending message to {} (fingerprint: {:02x?})", 
        recipient.name, &recipient.key_fingerprint[..8]);

    // 1. Generate unique message ID
    let mut rng = rand::thread_rng();
    let message_id = {
        let mut id = [0u8; 16];
        rng.fill_bytes(&mut id);
        id
    };

    // 2. Prepare recipient's public key
    let recipient_pk = pqc_kyber::PublicKey::from_bytes(&recipient.public_key)
        .map_err(|e| CryptoError::KeyError(format!("Invalid public key format: {:?}", e)))?;

    // 3. Generate ephemeral keypair for forward secrecy
    let (ephemeral_pk, ephemeral_sk) = keypair(&mut rng)
        .map_err(|e| CryptoError::KeyError(format!("Failed to generate ephemeral key: {:?}", e)))?;

    // 4. Encapsulate shared secret
    let (kyber_ciphertext, mut shared_secret) = encapsulate(&recipient_pk, &mut rng)
        .map_err(|e| CryptoError::EncryptionError(format!("Kyber encapsulation failed: {:?}", e)))?;

    // 5. Derive encryption keys using HKDF
    let aes_key = derive_keys(&shared_secret, &message_id, b"aes_key");
    let hmac_key = derive_keys(&shared_secret, &message_id, b"hmac_key");
    
    // Zeroize shared secret immediately after use
    shared_secret.zeroize();

    // 6. Prepare payload
    let timestamp = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_err(|_| CryptoError::InvalidInput("System time error".to_string()))?
        .as_secs();

    let payload = MessagePayload {
        text: message.to_string(),
        timestamp,
        sender_name: sender_identity.name.clone(),
        ephemeral_public_key: ephemeral_pk.to_vec(),
    };

    // 7. Encrypt payload
    let nonce = {
        let mut nonce = [0u8; 12];
        rng.fill_bytes(&mut nonce);
        nonce
    };

    let encrypted_payload = encrypt_payload(&payload, &aes_key, &nonce)
        .map_err(|e| CryptoError::EncryptionError(format!("Payload encryption failed: {}", e)))?;

    // 8. Create packet
    let mut packet = EncryptedPacket {
        version: PROTOCOL_VERSION,
        kyber_ciphertext: kyber_ciphertext.to_vec(),
        encrypted_payload,
        nonce,
        hmac: [0u8; 32],
        timestamp,
        message_id,
        sender_fingerprint: sender_identity.key_fingerprint,
    };

    // 9. Calculate HMAC
    packet.hmac = calculate_hmac(&packet, &hmac_key)
        .map_err(|e| CryptoError::EncryptionError(format!("HMAC calculation failed: {}", e)))?;

    // 10. Serialize and send
    let serialized = bincode::serialize(&packet)
        .map_err(CryptoError::SerializationError)?;

    // 11. Send via Veilid with retry logic
    send_with_retry(network_context, route_id, &serialized).await
        .map_err(|e| CryptoError::NetworkError(format!("Failed to send message: {}", e)))?;

    log::info!("Message {} sent successfully", hex::encode(&message_id[..8]));
    
    // 12. Clean up ephemeral key
    ephemeral_sk.zeroize();

    Ok(message_id)
}

We can simply wrap messages sent from user to user in a public key encryption style way.

https://users.Rust-lang.org/t/ann-ml-kem-v0-2-0-pure-Rust-implementation-of-the-fips-203-final-post-quantum-kem-construction-formerly-known-as-kyber/116111

Conclusion: #

There is a lot of mixnets coming out such as Nym, xx network, hopr etc, but I think in the near future Veilid will grow and grow. Will it motivate people to run nodes based Crypto Economical game theory? No… But neither has Tor. In the future, we hope to see a lot more of adoption of Veilid, resulting in more user application adopting Veilid with more use cases.
We have managed to get a Desktop UI with iced.rs, network routing via Veilid and we have added some extra magic to encrypt messages going from user-to-user.

Good resources: #

https://gitlab.com/veilid/veilid/-/blob/main/veilid-core/README.md
https://veilid.gitlab.io/developer-book/index.html
https://veilid.com/chat/
https://en.wikipedia.org/wiki/ZeroNet
https://blog.Rust.careers/post/veilid_dildog_Rust_interview/
https://darkrenaissance.github.io/darkfi/misc/darkirc/darkirc.html
https://en.wikipedia.org/wiki/Forward_secrecy
https://csrc.nist.gov/glossary/term/perfect_forward_secrecy

Code repo of final version(will be published soon here): #

https://github.com/flipchan/EncryptedMsg