Skip to content

Commit a4dcfa9

Browse files
committed
feat: Add session model and implement session initialization in SessionService
1 parent 2bd0122 commit a4dcfa9

4 files changed

Lines changed: 230 additions & 21 deletions

File tree

lib/data/node/sessions/create_session.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ Future<bool> createSession(String nodeUrl, String recipientUserId) async {
155155
'ephemeral_pubkey': ekPubB64,
156156
'sender_identity_pub': base64Encode(ikPub),
157157
'ciphertext': ciphertextB64,
158+
'otpk_used': device.oneTimePrekeyPub,
158159
'sender_prekey_pub': base64Encode(ikPub),
159160
});
160161
} // end for devices

lib/models/session.dart

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import 'dart:convert';
2+
import 'dart:typed_data';
3+
4+
class Session {
5+
final String id;
6+
final String chatId;
7+
final String senderDeviceId;
8+
final String receiverDeviceId;
9+
final Uint8List rootKey;
10+
final Uint8List? sendChainKey;
11+
final Uint8List? recvChainKey;
12+
final int sendCounter;
13+
final int recvCounter;
14+
final Uint8List ratchetPub;
15+
final Uint8List? ratchetPriv;
16+
final Uint8List? lastRemotePub;
17+
final DateTime createdAt;
18+
final DateTime updatedAt;
19+
20+
Session({
21+
this.id = '',
22+
this.chatId = '',
23+
required this.senderDeviceId,
24+
required this.receiverDeviceId,
25+
required this.rootKey,
26+
this.sendChainKey,
27+
this.recvChainKey,
28+
this.sendCounter = 0,
29+
this.recvCounter = 0,
30+
required this.ratchetPub,
31+
this.ratchetPriv,
32+
this.lastRemotePub,
33+
required this.createdAt,
34+
required this.updatedAt,
35+
});
36+
37+
38+
factory Session.fromJson(Map<String, dynamic> json) {
39+
return Session(
40+
id: json['id'],
41+
chatId: json['chat_id'],
42+
senderDeviceId: json['sender_device_id'],
43+
receiverDeviceId: json['receiver_device_id'],
44+
rootKey: base64Decode(json['root_key']),
45+
sendChainKey: json['send_chain_key'] != null
46+
? base64Decode(json['send_chain_key'])
47+
: null,
48+
recvChainKey: json['recv_chain_key'] != null
49+
? base64Decode(json['recv_chain_key'])
50+
: null,
51+
sendCounter: json['send_counter'] ?? 0,
52+
recvCounter: json['recv_counter'] ?? 0,
53+
ratchetPub: base64Decode(json['ratchet_pub']),
54+
ratchetPriv: json['ratchet_priv'] != null
55+
? base64Decode(json['ratchet_priv'])
56+
: null,
57+
lastRemotePub: json['last_remote_pub'] != null
58+
? base64Decode(json['last_remote_pub'])
59+
: null,
60+
createdAt: DateTime.parse(json['created_at']),
61+
updatedAt: DateTime.parse(json['updated_at']),
62+
);
63+
}
64+
65+
Map<String, dynamic> toJson() {
66+
return {
67+
'id': id,
68+
'chat_id': chatId,
69+
'sender_device_id': senderDeviceId,
70+
'receiver_device_id': receiverDeviceId,
71+
'root_key': base64Encode(rootKey),
72+
if (sendChainKey != null)
73+
'send_chain_key': base64Encode(sendChainKey!),
74+
if (recvChainKey != null)
75+
'recv_chain_key': base64Encode(recvChainKey!),
76+
'send_counter': sendCounter,
77+
'recv_counter': recvCounter,
78+
'ratchet_pub': base64Encode(ratchetPub),
79+
if (ratchetPriv != null)
80+
'ratchet_priv': base64Encode(ratchetPriv!),
81+
if (lastRemotePub != null)
82+
'last_remote_pub': base64Encode(lastRemotePub!),
83+
'created_at': createdAt.toIso8601String(),
84+
'updated_at': updatedAt.toIso8601String(),
85+
};
86+
}
87+
Map<String, dynamic> toConfirmJson() {
88+
return {
89+
"sender_device_id": senderDeviceId,
90+
"receiver_device_id": receiverDeviceId,
91+
"root_key": base64Encode(rootKey),
92+
"ratchet_pub": base64Encode(ratchetPub),
93+
if (lastRemotePub != null)
94+
"last_remote_pub": base64Encode(lastRemotePub!),
95+
};
96+
}
97+
}

lib/services/key_provider.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class KeyProvider {
1515
final keyExchangeAlgorithm = X25519();
1616
static const _identityKey = 'identity_key';
1717
static const _preKey = 'pre_key';
18+
FlutterSecureStorage get secureStorage => _storage;
1819

1920
KeyProvider._internal();
2021
Future<void> initialize(ValueNotifier<int>? stepNotifier) async {

lib/services/session_service.dart

Lines changed: 131 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,23 @@ import 'package:cryptography/cryptography.dart';
44
import 'package:dio/dio.dart';
55
import 'package:flutter/foundation.dart';
66
import 'package:hushnet_frontend/models/pending_sessions.dart';
7+
import 'package:hushnet_frontend/models/session.dart';
78
import 'package:hushnet_frontend/services/key_provider.dart';
89
import 'package:hushnet_frontend/services/node_service.dart';
10+
import 'package:hushnet_frontend/services/secure_storage_service.dart';
911

1012
class SessionService {
1113
final KeyProvider keyProvider = KeyProvider();
1214
final Dio dio = Dio();
1315
final NodeService nodeService = NodeService();
16+
final SecureStorageService secureStorage = SecureStorageService();
1417

1518
Future<List<PendingSession>> getPendingSessions() async {
1619
final String? nodeUrl = await nodeService.getCurrentNodeUrl();
17-
final req = await keyProvider.sendSignedRequest("GET", "$nodeUrl/sessions/pending");
20+
final req = await keyProvider.sendSignedRequest(
21+
"GET",
22+
"$nodeUrl/sessions/pending",
23+
);
1824
final data = req.data;
1925
final List sessions = (data is List) ? data : (data['sessions'] ?? []);
2026
return sessions.map((s) => PendingSession.fromJson(s)).toList();
@@ -62,21 +68,36 @@ class SessionService {
6268
type: KeyPairType.x25519,
6369
);
6470

65-
final senderIkPub = SimplePublicKey(senderPrekeyPub, type: KeyPairType.x25519);
66-
final ekAPub = SimplePublicKey(senderEphemeral, type: KeyPairType.x25519);
71+
final senderIkPub = SimplePublicKey(
72+
senderPrekeyPub,
73+
type: KeyPairType.x25519,
74+
);
75+
final ekAPub = SimplePublicKey(
76+
senderEphemeral,
77+
type: KeyPairType.x25519,
78+
);
6779
debugPrint("Alice EK pub: ${base64Encode(senderEphemeral)}");
6880
debugPrint("Bob IK Priv: ${base64Encode(ikPriv)}");
6981

7082
// DH1 = DH(SPK_B, IK_A)
71-
final dh1 = await x25519.sharedSecretKey(keyPair: spkPair, remotePublicKey: senderIkPub);
83+
final dh1 = await x25519.sharedSecretKey(
84+
keyPair: spkPair,
85+
remotePublicKey: senderIkPub,
86+
);
7287
final dh1Bytes = await dh1.extractBytes();
7388

7489
// DH2 = DH(IK_B, EK_A)
75-
final dh2 = await x25519.sharedSecretKey(keyPair: ikPair, remotePublicKey: ekAPub);
90+
final dh2 = await x25519.sharedSecretKey(
91+
keyPair: ikPair,
92+
remotePublicKey: ekAPub,
93+
);
7694
final dh2Bytes = await dh2.extractBytes();
7795

7896
// DH3 = DH(SPK_B, EK_A)
79-
final dh3 = await x25519.sharedSecretKey(keyPair: spkPair, remotePublicKey: ekAPub);
97+
final dh3 = await x25519.sharedSecretKey(
98+
keyPair: spkPair,
99+
remotePublicKey: ekAPub,
100+
);
80101
final dh3Bytes = await dh3.extractBytes();
81102

82103
// (optional) DH4 = DH(OPK_B, EK_A)
@@ -85,10 +106,16 @@ class SessionService {
85106
final opk = oneTimePreKeys.first;
86107
final opkPair = SimpleKeyPairData(
87108
opk['private']!,
88-
publicKey: SimplePublicKey(opk['public']!, type: KeyPairType.x25519),
109+
publicKey: SimplePublicKey(
110+
opk['public']!,
111+
type: KeyPairType.x25519,
112+
),
89113
type: KeyPairType.x25519,
90114
);
91-
final dh4 = await x25519.sharedSecretKey(keyPair: opkPair, remotePublicKey: ekAPub);
115+
final dh4 = await x25519.sharedSecretKey(
116+
keyPair: opkPair,
117+
remotePublicKey: ekAPub,
118+
);
92119
dh4Bytes = await dh4.extractBytes();
93120
}
94121

@@ -99,22 +126,19 @@ class SessionService {
99126
debugPrint('DH4 length: ${dh4Bytes.length}');
100127
debugPrint('Combined length: ${combined.length}');
101128
debugPrint('DH1: ${base64Encode(dh1Bytes)}');
102-
debugPrint('DH2: ${base64Encode(dh2Bytes)}');
103-
debugPrint('DH3: ${base64Encode(dh3Bytes)}');
104-
debugPrint('DH4: ${base64Encode(dh4Bytes)}');
105-
106-
final rootKey = await hkdf.deriveKey(
107-
secretKey: SecretKey(combined),
108-
nonce: utf8.encode('HushNet-Salt'), // facultatif mais recommandé
109-
info: utf8.encode('X3DH Root Key'),
110-
);
129+
debugPrint('DH2: ${base64Encode(dh2Bytes)}');
130+
debugPrint('DH3: ${base64Encode(dh3Bytes)}');
131+
debugPrint('DH4: ${base64Encode(dh4Bytes)}');
132+
133+
final rootKey = await hkdf.deriveKey(
134+
secretKey: SecretKey(combined),
135+
nonce: utf8.encode('HushNet-Salt'), // facultatif mais recommandé
136+
info: utf8.encode('X3DH Root Key'),
137+
);
111138

112139
final plaintext = await _decryptCiphertext(p.ciphertext, rootKey, aes);
113140
debugPrint('✅ Decrypted pending session ${p.id}: $plaintext');
114-
115-
// Delete pending session on server
116-
// final String? nodeUrl = await nodeService.getCurrentNodeUrl();
117-
// await dio.delete('$nodeUrl/sessions/${p.id}/complete');
141+
await initializeRatchetSession(p, rootKey);
118142
} catch (e) {
119143
debugPrint('❌ Failed to process ${p.id}: $e');
120144
}
@@ -138,4 +162,90 @@ final rootKey = await hkdf.deriveKey(
138162
final clear = await aes.decrypt(box, secretKey: rootKey);
139163
return utf8.decode(clear);
140164
}
165+
166+
Future<void> uploadSession(Session session, PendingSession pending) async {
167+
final String? nodeUrl = await nodeService.getCurrentNodeUrl();
168+
final req = await keyProvider.sendSignedRequest(
169+
"POST",
170+
"$nodeUrl/sessions/confirm",
171+
payload: {
172+
"pending_session_id": pending.id,
173+
...session.toConfirmJson(),
174+
},
175+
);
176+
debugPrint('Uploaded session: ${req.statusCode}');
177+
debugPrint('Response data: ${req.data}');
178+
}
179+
180+
Future<void> initializeRatchetSession(
181+
PendingSession pending,
182+
SecretKey rootKey,
183+
) async {
184+
// derive send/recv chain keys
185+
try {
186+
final hkdf = Hkdf(hmac: Hmac.sha256(), outputLength: 32);
187+
final rootBytes = await rootKey.extractBytes();
188+
189+
final sendChainKey = await hkdf.deriveKey(
190+
secretKey: SecretKeyData(rootBytes),
191+
nonce: utf8.encode('HushNet-Salt'),
192+
info: utf8.encode('HushNet-Send-Chain'),
193+
);
194+
195+
final recvChainKey = await hkdf.deriveKey(
196+
secretKey: SecretKeyData(rootBytes),
197+
nonce: utf8.encode('HushNet-Salt'),
198+
info: utf8.encode('HushNet-Recv-Chain'),
199+
);
200+
201+
final ratchetAlgo = X25519();
202+
final ratchetPair = await ratchetAlgo.newKeyPair();
203+
final ratchetPub = await ratchetPair.extractPublicKey();
204+
final ratchetPriv = await ratchetPair.extractPrivateKeyBytes();
205+
206+
final rootKeyB64 = base64Encode(rootBytes);
207+
final sendChainB64 = base64Encode(await sendChainKey.extractBytes());
208+
final recvChainB64 = base64Encode(await recvChainKey.extractBytes());
209+
final ratchetPubB64 = base64Encode(ratchetPub.bytes);
210+
final ratchetPrivB64 = base64Encode(ratchetPriv);
211+
212+
await keyProvider.secureStorage.write(
213+
key: "session_${pending.senderDeviceId}_root",
214+
value: rootKeyB64,
215+
);
216+
await keyProvider.secureStorage.write(
217+
key: "session_${pending.senderDeviceId}_send_chain",
218+
value: sendChainB64,
219+
);
220+
await keyProvider.secureStorage.write(
221+
key: "session_${pending.senderDeviceId}_recv_chain",
222+
value: recvChainB64,
223+
);
224+
await keyProvider.secureStorage.write(
225+
key: "session_${pending.senderDeviceId}_ratchet_pub",
226+
value: ratchetPubB64,
227+
);
228+
await keyProvider.secureStorage.write(
229+
key: "session_${pending.senderDeviceId}_ratchet_priv",
230+
value: ratchetPrivB64,
231+
);
232+
debugPrint(
233+
'✅ Initialized ratchet session with ${pending.senderDeviceId}',
234+
);
235+
final session = Session(
236+
senderDeviceId: pending.senderDeviceId,
237+
receiverDeviceId: pending.recipientDeviceId,
238+
rootKey: Uint8List.fromList(rootBytes),
239+
ratchetPub: Uint8List.fromList(ratchetPub.bytes),
240+
lastRemotePub: Uint8List.fromList(
241+
base64Decode(pending.ephemeralPubkey),
242+
),
243+
createdAt: DateTime.now(),
244+
updatedAt: DateTime.now(),
245+
);
246+
await uploadSession(session, pending);
247+
} catch (e) {
248+
debugPrint('Error initializing ratchet session: $e');
249+
}
250+
}
141251
}

0 commit comments

Comments
 (0)