Skip to content

Commit bcf1f79

Browse files
committed
feat: refactor session management by removing unnecessary keys and adding pending sessions table
1 parent be0a6c2 commit bcf1f79

8 files changed

Lines changed: 111 additions & 82 deletions

File tree

sql_models/seed.sql

Lines changed: 103 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,163 @@
1+
-- Enable UUID generator
12
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
23

4+
-- =========================
5+
-- Users
6+
-- =========================
37
CREATE TABLE users (
48
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
59
username TEXT UNIQUE NOT NULL,
610
created_at TIMESTAMP DEFAULT NOW()
711
);
812

13+
-- =========================
14+
-- Devices
15+
-- =========================
916
CREATE TABLE devices (
1017
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1118
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
12-
identity_pubkey TEXT NOT NULL,
19+
identity_pubkey TEXT NOT NULL,
1320
prekey_pubkey TEXT NOT NULL,
1421
signed_prekey_pub TEXT NOT NULL,
1522
signed_prekey_sig TEXT NOT NULL,
16-
one_time_prekeys JSONB NOT NULL,
23+
one_time_prekeys JSONB NOT NULL, -- array of one-time public prekeys
1724
device_label TEXT,
1825
push_token TEXT,
1926
last_seen TIMESTAMP DEFAULT NOW(),
2027
created_at TIMESTAMP DEFAULT NOW(),
21-
UNIQUE(user_id, identity_pubkey)
28+
UNIQUE (user_id, identity_pubkey)
2229
);
2330

31+
-- =========================
32+
-- Chats
33+
-- =========================
2434
CREATE TABLE chats (
25-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
26-
user_a_id UUID REFERENCES users(id) ON DELETE CASCADE,
27-
user_b_id UUID REFERENCES users(id) ON DELETE CASCADE,
28-
created_at TIMESTAMP DEFAULT NOW(),
29-
UNIQUE(user_a_id, user_b_id)
35+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
36+
type TEXT CHECK (type IN ('direct','group')) DEFAULT 'direct',
37+
38+
-- Direct chat participants
39+
user_a UUID REFERENCES users(id) ON DELETE CASCADE,
40+
user_b UUID REFERENCES users(id) ON DELETE CASCADE,
41+
42+
-- Group chat info
43+
name TEXT,
44+
owner_id UUID REFERENCES users(id) ON DELETE SET NULL,
45+
46+
-- Will be linked after messages table is created
47+
last_message_id UUID,
48+
49+
created_at TIMESTAMP DEFAULT NOW(),
50+
updated_at TIMESTAMP DEFAULT NOW(),
51+
52+
-- For direct chats: both users must be set, and enforce strict ordering
53+
CONSTRAINT chats_direct_shape
54+
CHECK (
55+
type <> 'direct'
56+
OR (user_a IS NOT NULL AND user_b IS NOT NULL AND user_a < user_b)
57+
)
3058
);
3159

60+
-- Prevent duplicates (A,B) vs (B,A)
61+
CREATE UNIQUE INDEX uniq_direct_chat
62+
ON chats (LEAST(user_a, user_b), GREATEST(user_a, user_b))
63+
WHERE type = 'direct';
3264

33-
CREATE TABLE messages (
34-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
65+
-- =========================
66+
-- Chat Members (for group chats)
67+
-- =========================
68+
CREATE TABLE chat_members (
3569
chat_id UUID REFERENCES chats(id) ON DELETE CASCADE,
36-
from_device_id UUID REFERENCES devices(id) ON DELETE SET NULL,
37-
plaintext_hash TEXT, -- SHA256 du plaintext avant chiffrement
38-
created_at TIMESTAMP DEFAULT NOW()
70+
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
71+
role TEXT DEFAULT 'member',
72+
joined_at TIMESTAMP DEFAULT NOW(),
73+
PRIMARY KEY (chat_id, user_id)
3974
);
4075

41-
CREATE TABLE message_deliveries (
76+
-- =========================
77+
-- Messages
78+
-- =========================
79+
CREATE TABLE messages (
4280
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
43-
message_id UUID REFERENCES messages(id) ON DELETE CASCADE,
81+
82+
-- Logical message id (shared by all device fanouts)
83+
logical_msg_id TEXT NOT NULL,
84+
85+
chat_id UUID REFERENCES chats(id) ON DELETE CASCADE,
86+
87+
from_user_id UUID REFERENCES users(id) ON DELETE CASCADE,
88+
from_device_id UUID REFERENCES devices(id) ON DELETE CASCADE,
89+
90+
to_user_id UUID REFERENCES users(id) ON DELETE CASCADE,
4491
to_device_id UUID REFERENCES devices(id) ON DELETE CASCADE,
45-
ciphertext TEXT NOT NULL,
46-
delivered BOOLEAN DEFAULT FALSE,
92+
93+
header JSONB NOT NULL, -- Double Ratchet header (DH pubkey, counters, etc.)
94+
ciphertext TEXT NOT NULL, -- base64(nonce || cipher || mac)
95+
96+
delivered_at TIMESTAMP,
97+
read_at TIMESTAMP,
4798
created_at TIMESTAMP DEFAULT NOW()
4899
);
49100

101+
-- Add FK once messages table exists
102+
ALTER TABLE chats
103+
ADD CONSTRAINT fk_chats_last_message
104+
FOREIGN KEY (last_message_id)
105+
REFERENCES messages(id)
106+
ON DELETE SET NULL;
107+
108+
-- Indexes
109+
CREATE INDEX idx_messages_todevice ON messages(to_device_id, created_at);
110+
CREATE INDEX idx_messages_chatid ON messages(chat_id, created_at);
111+
CREATE INDEX idx_messages_logical ON messages(logical_msg_id);
50112

113+
-- =========================
114+
-- Sessions (metadata only, no secret keys)
115+
-- =========================
51116
CREATE TABLE sessions (
52117
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
53118
chat_id UUID REFERENCES chats(id) ON DELETE CASCADE,
54119
sender_device_id UUID REFERENCES devices(id) ON DELETE CASCADE,
55120
receiver_device_id UUID REFERENCES devices(id) ON DELETE CASCADE,
56-
root_key BYTEA NOT NULL,
57-
send_chain_key BYTEA,
58-
recv_chain_key BYTEA,
59-
send_counter INTEGER DEFAULT 0,
60-
recv_counter INTEGER DEFAULT 0,
61-
ratchet_pub BYTEA,
62-
last_remote_pub BYTEA,
63121
created_at TIMESTAMP DEFAULT NOW(),
64122
updated_at TIMESTAMP DEFAULT NOW(),
65-
UNIQUE(sender_device_id, receiver_device_id)
123+
UNIQUE (sender_device_id, receiver_device_id)
66124
);
67125

126+
-- =========================
127+
-- Used Tokens (anti-replay or enrollment)
128+
-- =========================
68129
CREATE TABLE used_tokens (
69130
token TEXT PRIMARY KEY,
70131
used_at TIMESTAMP DEFAULT NOW()
71132
);
72133

73-
134+
-- =========================
135+
-- Pending Sessions (X3DH handshake initialization)
136+
-- =========================
74137
CREATE TABLE pending_sessions (
75138
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
76-
sender_device_id UUID NOT NULL REFERENCES devices(id),
77-
recipient_device_id UUID NOT NULL REFERENCES devices(id),
78-
ephemeral_pubkey TEXT NOT NULL,
79-
sender_prekey_pub TEXT NOT NULL,
80-
otpk_used TEXT NOT NULL,
81-
ciphertext TEXT NOT NULL,
139+
sender_device_id UUID NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
140+
recipient_device_id UUID NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
141+
ephemeral_pubkey TEXT NOT NULL, -- Ephemeral key (EK_A pub)
142+
sender_prekey_pub TEXT NOT NULL, -- IK/SPK_A used by sender
143+
otpk_used TEXT NOT NULL, -- Whether a one-time prekey was consumed
144+
ciphertext TEXT NOT NULL, -- Encrypted payload (init message)
82145
created_at TIMESTAMP DEFAULT NOW(),
83-
state TEXT DEFAULT 'initiated' CHECK (state IN ('initiated', 'responded', 'completed'))
146+
state TEXT DEFAULT 'initiated' CHECK (state IN ('initiated','responded','completed'))
84147
);
85148

86-
ALTER TABLE chats
87-
ADD CONSTRAINT chats_unique_pair CHECK (user_a_id < user_b_id);
88-
149+
CREATE INDEX idx_pending_sessions_recipient ON pending_sessions(recipient_device_id, created_at);
89150

151+
-- =========================
152+
-- Convenience View: Public device list
153+
-- =========================
90154
CREATE VIEW user_devices_view AS
91-
SELECT
155+
SELECT
92156
u.username,
93157
d.id AS device_id,
94158
d.identity_pubkey,
95159
d.signed_prekey_pub,
96160
d.signed_prekey_sig,
97-
(d.one_time_prekeys -> 0) AS one_time_prekey_pub
161+
(d.one_time_prekeys ->> 0) AS one_time_prekey_pub
98162
FROM users u
99-
JOIN devices d ON u.id = d.user_id;
163+
JOIN devices d ON u.id = d.user_id;

src/controllers/session_controller.rs

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,6 @@ pub struct ConfirmSessionBody {
3030
pub pending_session_id: Uuid,
3131
pub sender_device_id: Uuid,
3232
pub receiver_device_id: Uuid,
33-
pub root_key: String,
34-
pub ratchet_pub: String,
35-
pub last_remote_pub: String,
3633
}
3734

3835

@@ -96,7 +93,6 @@ pub async fn confirm_session(
9693
AuthenticatedDevice(device): AuthenticatedDevice,
9794
Json(payload): Json<ConfirmSessionBody>,
9895
) -> Result<impl IntoResponse, (StatusCode, &'static str)> {
99-
// 1️⃣ Vérifie que la pending session existe et appartient au device courant
10096
let pending_session = session_repository::get_pending_session_by_id(
10197
&state.pool,
10298
&payload.pending_session_id,
@@ -110,19 +106,6 @@ pub async fn confirm_session(
110106
None => return Err((StatusCode::NOT_FOUND, "Pending session not found or not owned by device")),
111107
};
112108

113-
// 2️⃣ Decode les clés Base64 -> bytes
114-
let root_key_bytes = general_purpose::STANDARD
115-
.decode(&payload.root_key)
116-
.map_err(|_| (StatusCode::BAD_REQUEST, "Invalid root_key"))?;
117-
118-
let ratchet_pub_bytes = general_purpose::STANDARD
119-
.decode(&payload.ratchet_pub)
120-
.map_err(|_| (StatusCode::BAD_REQUEST, "Invalid ratchet_pub"))?;
121-
122-
let last_remote_pub_bytes = general_purpose::STANDARD
123-
.decode(&payload.last_remote_pub)
124-
.map_err(|_| (StatusCode::BAD_REQUEST, "Invalid last_remote_pub"))?;
125-
126109
// 3️⃣ Récupère ou crée le chat_id
127110
let chat_id = session_repository::get_or_create_chat_id(
128111
&state.pool,
@@ -141,10 +124,7 @@ pub async fn confirm_session(
141124
&state.pool,
142125
&chat_id,
143126
&payload.sender_device_id,
144-
&payload.receiver_device_id,
145-
&root_key_bytes,
146-
&ratchet_pub_bytes,
147-
&last_remote_pub_bytes,
127+
&payload.receiver_device_id
148128
)
149129
.await
150130
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Failed to insert session"))?;

src/models/message.rs

Whitespace-only changes.

src/models/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ pub mod enrollment_token;
33
pub mod keys;
44
pub mod session;
55
pub mod user;
6+
pub mod message;

src/models/session.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,6 @@ pub struct Session {
2020
pub chat_id: Option<Uuid>,
2121
pub sender_device_id: Uuid,
2222
pub receiver_device_id: Uuid,
23-
pub root_key: Vec<u8>,
24-
pub send_chain_key: Option<Vec<u8>>,
25-
pub recv_chain_key: Option<Vec<u8>>,
26-
pub send_counter: i32,
27-
pub recv_counter: i32,
28-
pub ratchet_pub: Option<Vec<u8>>,
29-
pub last_remote_pub: Option<Vec<u8>>,
3023
pub created_at: Option<NaiveDateTime>,
3124
pub updated_at: Option<NaiveDateTime>,
3225
}

src/repository/message_repository.rs

Whitespace-only changes.

src/repository/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ pub mod enrollment_token_repository;
33
pub mod keys_repository;
44
pub mod session_repository;
55
pub mod user_repository;
6+
pub mod message_repository;

src/repository/session_repository.rs

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,11 @@ pub async fn get_or_create_chat_id(
106106
if let Some(chat_id) = sqlx::query_scalar!(
107107
r#"
108108
SELECT id FROM chats
109-
WHERE (user_a_id = LEAST(
109+
WHERE (user_a= LEAST(
110110
(SELECT user_id FROM devices WHERE id=$1),
111111
(SELECT user_id FROM devices WHERE id=$2)
112112
)
113-
AND user_b_id = GREATEST(
113+
AND user_b = GREATEST(
114114
(SELECT user_id FROM devices WHERE id=$1),
115115
(SELECT user_id FROM devices WHERE id=$2)
116116
))
@@ -126,7 +126,7 @@ pub async fn get_or_create_chat_id(
126126
// sinon crée-le proprement en respectant la contrainte
127127
let new_chat_id = sqlx::query_scalar!(
128128
r#"
129-
INSERT INTO chats (user_a_id, user_b_id)
129+
INSERT INTO chats (user_a, user_b)
130130
VALUES (
131131
LEAST(
132132
(SELECT user_id FROM devices WHERE id=$1),
@@ -153,30 +153,20 @@ pub async fn insert_or_update_session(
153153
chat_id: &Uuid,
154154
sender_device_id: &Uuid,
155155
receiver_device_id: &Uuid,
156-
root_key: &[u8],
157-
ratchet_pub: &[u8],
158-
last_remote_pub: &[u8],
159156
) -> Result<(), sqlx::Error> {
160157
sqlx::query!(
161158
r#"
162159
INSERT INTO sessions (
163-
chat_id, sender_device_id, receiver_device_id,
164-
root_key, ratchet_pub, last_remote_pub
160+
chat_id, sender_device_id, receiver_device_id
165161
)
166-
VALUES ($1, $2, $3, $4, $5, $6)
162+
VALUES ($1, $2, $3)
167163
ON CONFLICT (sender_device_id, receiver_device_id)
168164
DO UPDATE SET
169-
root_key = EXCLUDED.root_key,
170-
ratchet_pub = EXCLUDED.ratchet_pub,
171-
last_remote_pub = EXCLUDED.last_remote_pub,
172165
updated_at = NOW()
173166
"#,
174167
chat_id,
175168
sender_device_id,
176169
receiver_device_id,
177-
root_key,
178-
ratchet_pub,
179-
last_remote_pub
180170
)
181171
.execute(pool)
182172
.await?;

0 commit comments

Comments
 (0)