Now onto the actual plan of the chat application.
The user data will be stored in an SQLcipher database, which is basically an encrypted SQLite database. I remember messing with it before in Rust, it was a pain in the ass to get it work on Windows. (This reminded me to check that Win10 VM I haven't booted up in years... I guess it's no longer updated anymore.) This database will be unlocked upon starting the application with a password.
In this section I gather the amount of things that need to be stored in the database. The list is not comprehensive. As I may more ideas on the go. So I've enumerated here what I've come up with. As we previously played with it, each user will have a CA certificate that identifies the user. Then each user has device certificates that are signed by this user CA. And user can have arbitrary amount of devices, they can communicate with us and vice versa as long as the device certificate is signed by the user CA key.
In the following description I will use the Rust struct notation.
Since I will use SQLCipher this isn't how it these things will appear in code.
The Vec<> is used to denote lists.
The Option<> is used to denote optional, nullable members.
Let's start at the top level:
struct UserData {
self_info: SelfInfo,
messages: Vec<Message>,
contact: Vec<Contact>
}
SelfInfo
struct SelfInfo {
dev_private_key: PrivateKey,
device_certificate: Certificate,
user_ca_certificate: Certificate,
primary_device_info: Option<PrimaryDeviceInfo>,
access_methods: Vec<AccessMethod>,
user_metadata: UserMetadata,
historical_certificates: Vec<Certificate>, // for certificates that were used in past in this device.
last_update: Timestamp
}
Message
struct Message {
message_data: MessageData,
sender_ca_id: CertificateId,
receiver_ca_id: CertificateId,
sender_device_cert_id: CertificateId, // for debug purposes only
created_at: Timestamp,
received_at: Option<Timestamp>, // set when the message is confirmed to be received.
message_direction: MessageDirection, // Incoming or outgoing.
}
For incoming messages the created and received timestamp is the same, because the message is created on receipt. For outgoing messages the received timestamp is set when at least one device of the recipient confirmed the receipt of the message. All timestamps in this data model refer to the timestamp of the current node the data is on (unless noted otherwise).
Contact
struct Contact {
user_ca: Certificate,
device_infos: Vec<ContactDeviceInfo>,
}
PrivateKey and CertificateThese are PEM encoded private keys and certificates, they are stored as strings.
PrimaryDeviceInfoThe primary device information is relevant for devices that hold the user CA key.
struct PrimaryDeviceInfo {
user_ca_private_key: PrivateKey,
certificates_signed: Vec<Certificate>,
certificates_revoked: Vec<Certificate>,
last_updated: Timestamp
}
AccessMethodThe access method describes how can an active node be accessed.
struct AccessMethod {
host_name: String,
port_number: u16,
server_name: String, // to be used for TLS SNI,
last_updated: Timestamp,
}
In the future I'm considering adding other access methods, such as Tor hidden services. I'm also considering adding support to some kind of DNS alternative, so one doesn't even need to depend on the centralized DNS used on the internet if they are not available.
UserMetadata
struct UserMetadata {
device_motd: String,
}
So one can leave a message for each device.
TimestampThis is a Unix timestamp, used for local times only.
MessageDataThe message itself. For the prototype it will be a string. Later it this can be expanded to other kinds of data too, such as voice messages or videos, (keep in mind that large files probably shouldn't be stored in the database.)
CertificateIdThe hash "thumb print" of the certificate.
MessageDirection
enum MessageDirection {
Incoming,
Outgoing
}
ContactDeviceInfoThis contains information what we know about the device of the contact.
struct ContactDeviceInfo {
device_cert: Certificate,
device_access_states: Vec<DeviceAccessState>,
last_updated: Timestamp,
}
When a device comes online (it contacts us, or we manage to connect to it), the timestamp of the last update is consulted and based on that, our device sends all the messages that wasn't received yet, and sends other updates too.
DeviceAccessStateThis contains information about the number of failed attempts to contact this device. This applies to only devices that have access method, therefore they are active. The current device will repeatedly try to reach the device on the given access methods. If the access fails, then the timestamp of the last attempt and the number of attempts are stored. After each failed attempts the retry period is doubled. At first the retry period is 1 minute, then it doubles until it reaches 8192, the the given access method is declared dead and the access method is removed. That's roughly 2 months of trying.
struct DeviceAccessState {
access_method: AccessMethod, // to access this device.
last_attempt: Option<Timestamp>
number_of_failed_attempts: u32,
}
The String, u16 and u32 types are the same as the built in types in Rust.
Aaaand 2 hours have passed already writing this up. In the next post I will continue designing the various operations that can should be done on this data.
See the latest posts below, click the "..." to see them all. Click the tags to filter by tag. You can also subscribe to RSS in those lists.
Double entry bookkeeping explained - english finance
Chat2026 part 10: continuing application design - english chat2026-devblog
If you want privacy, please use a desktop PC - english privacy
YouTube is now practically unsearchable - english rants
Chat2026 part 9: using CRL in the server and client examples - english chat2026-devblog