Skip to content

Commit 2bd0122

Browse files
committed
feat: Implement session management and user device handling features
1 parent a2987df commit 2bd0122

11 files changed

Lines changed: 1014 additions & 1 deletion
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import 'dart:convert';
2+
import 'dart:typed_data';
3+
import 'package:cryptography/cryptography.dart';
4+
import 'package:dio/dio.dart';
5+
import 'package:flutter/material.dart';
6+
import 'package:hushnet_frontend/services/key_provider.dart';
7+
8+
/// Full X3DH + initial AES-GCM encrypt for each recipient device, then POST /sessions
9+
Future<bool> createSession(String nodeUrl, String recipientUserId) async {
10+
final keyProvider = KeyProvider();
11+
final dio = Dio();
12+
13+
try {
14+
// 1) load identity (Ed25519) and X25519 preKey (we use preKey as IK for DH)
15+
final identityKeyPair = await keyProvider
16+
.getIdentityKeyPair(); // Ed25519 pair bytes
17+
final preKeyPair = await keyProvider.getPreKeyPair(); // X25519 pair bytes
18+
if (identityKeyPair == null || preKeyPair == null) {
19+
throw Exception('Missing identity or prekey on client');
20+
}
21+
22+
// 2) get recipient devices (assumes these return X25519 pubs)
23+
final devices = await keyProvider.getUserDevicesKeys(recipientUserId);
24+
if (devices.isEmpty) {
25+
debugPrint('No devices for recipient');
26+
return false;
27+
}
28+
29+
final x25519 = X25519();
30+
final aes = AesGcm.with256bits();
31+
32+
final sessionsInit = <Map<String, dynamic>>[];
33+
34+
// Loop on each device
35+
for (final device in devices) {
36+
// Parse recipient pubs from base64 -> bytes
37+
final recipientIdentityPub = base64Decode(device.prekeyPubkey); // must be X25519 bytes
38+
final recipientSpkPub = base64Decode(device.signedPrekeyPub);
39+
final recipientOpkPub = device.oneTimePrekeyPub != null
40+
? base64Decode(device.oneTimePrekeyPub!)
41+
: null;
42+
43+
44+
// 1. generate ephemeral keypair (X25519)
45+
final ek = await keyProvider
46+
.generateEphemeralKeyPair(); // returns {'private', 'public'} bytes
47+
final ekPriv = ek['private']!;
48+
final ekPub = ek['public']!;
49+
50+
// 2. prepare local preKey (used as IK_A in our scheme)
51+
final ikPriv = preKeyPair['private']!;
52+
final ikPub = preKeyPair['public']!;
53+
54+
// 3. compute DHs
55+
// Build SimpleKeyPairData for our private keys
56+
final ikKeyPairData = SimpleKeyPairData(
57+
ikPriv,
58+
publicKey: SimplePublicKey(ikPub, type: KeyPairType.x25519),
59+
type: KeyPairType.x25519,
60+
);
61+
final ekKeyPairData = SimpleKeyPairData(
62+
ekPriv,
63+
publicKey: SimplePublicKey(ekPub, type: KeyPairType.x25519),
64+
type: KeyPairType.x25519,
65+
);
66+
67+
// remote public keys
68+
final recipientSpkPublic = SimplePublicKey(
69+
recipientSpkPub,
70+
type: KeyPairType.x25519,
71+
);
72+
final recipientIkPublic = SimplePublicKey(
73+
recipientIdentityPub,
74+
type: KeyPairType.x25519,
75+
);
76+
77+
// DH1 = DH(IK_A, SPK_B)
78+
final shared1 = await x25519.sharedSecretKey(
79+
keyPair: ikKeyPairData,
80+
remotePublicKey: recipientSpkPublic,
81+
);
82+
final bytes1 = await shared1.extractBytes();
83+
84+
// DH2 = DH(EK_A, IK_B)
85+
final shared2 = await x25519.sharedSecretKey(
86+
keyPair: ekKeyPairData,
87+
remotePublicKey: recipientIkPublic,
88+
);
89+
final bytes2 = await shared2.extractBytes();
90+
91+
// DH3 = DH(EK_A, SPK_B)
92+
final shared3 = await x25519.sharedSecretKey(
93+
keyPair: ekKeyPairData,
94+
remotePublicKey: recipientSpkPublic,
95+
);
96+
final bytes3 = await shared3.extractBytes();
97+
debugPrint("Alice EK pub: ${base64Encode(ekPub)}");
98+
debugPrint("Bob Signed PreKey pub: ${base64Encode(recipientSpkPub)}");
99+
// DH4 = DH(EK_A, OPK_B) if present
100+
List<int> bytes4 = [];
101+
if (recipientOpkPub != null) {
102+
final recipientOpkPublic = SimplePublicKey(
103+
recipientOpkPub,
104+
type: KeyPairType.x25519,
105+
);
106+
final shared4 = await x25519.sharedSecretKey(
107+
keyPair: ekKeyPairData,
108+
remotePublicKey: recipientOpkPublic,
109+
);
110+
bytes4 = await shared4.extractBytes();
111+
}
112+
113+
// 4. concatenate DH bytes
114+
final combined = <int>[];
115+
combined.addAll(bytes1);
116+
combined.addAll(bytes2);
117+
combined.addAll(bytes3);
118+
combined.addAll(bytes4);
119+
debugPrint('DH1: ${base64Encode(bytes1)}');
120+
debugPrint('DH2: ${base64Encode(bytes2)}');
121+
debugPrint('DH3: ${base64Encode(bytes3)}');
122+
debugPrint('DH4: ${base64Encode(bytes4)}');
123+
debugPrint('Combined DH length: ${combined.length}');
124+
125+
// 5. derive root key via HKDF-SHA256 (32 bytes)
126+
final hkdf = Hkdf(hmac: Hmac.sha256(), outputLength: 32);
127+
final rootKey = await hkdf.deriveKey(
128+
secretKey: SecretKeyData(combined),
129+
nonce: utf8.encode('HushNet-Salt'), // facultatif mais conseillé
130+
info: utf8.encode('X3DH Root Key'),
131+
);
132+
133+
// 6. encrypt initial plaintext with AES-GCM-256 using rootKey
134+
final nonce = aes.newNonce();
135+
final plaintext = utf8.encode(
136+
'HushNet initial session message',
137+
); // customize initial message as needed
138+
final secretBox = await aes.encrypt(
139+
plaintext,
140+
secretKey: rootKey,
141+
nonce: nonce,
142+
);
143+
144+
// Combine nonce + ciphertext + mac
145+
final ciphertextBytes = <int>[];
146+
ciphertextBytes.addAll(nonce);
147+
ciphertextBytes.addAll(secretBox.cipherText);
148+
ciphertextBytes.addAll(secretBox.mac.bytes);
149+
150+
final ciphertextB64 = base64Encode(Uint8List.fromList(ciphertextBytes));
151+
final ekPubB64 = base64Encode(ekPub);
152+
153+
sessionsInit.add({
154+
'recipient_device_id': device.deviceId,
155+
'ephemeral_pubkey': ekPubB64,
156+
'sender_identity_pub': base64Encode(ikPub),
157+
'ciphertext': ciphertextB64,
158+
'sender_prekey_pub': base64Encode(ikPub),
159+
});
160+
} // end for devices
161+
162+
// 7) sign timestamp with Ed25519 identity key for authentication headers
163+
final timestamp = (DateTime.now().millisecondsSinceEpoch ~/ 1000)
164+
.toString();
165+
final ed = Ed25519();
166+
167+
final signingPair = SimpleKeyPairData(
168+
identityKeyPair['private']!,
169+
publicKey: SimplePublicKey(
170+
identityKeyPair['public']!,
171+
type: KeyPairType.ed25519,
172+
),
173+
type: KeyPairType.ed25519,
174+
);
175+
final signature = await ed.sign(
176+
utf8.encode(timestamp),
177+
keyPair: signingPair,
178+
);
179+
180+
final headers = {
181+
'X-Identity-Key': base64Encode(identityKeyPair['public']!),
182+
'X-Timestamp': timestamp,
183+
'X-Signature': base64Encode(signature.bytes),
184+
'Content-Type': 'application/json',
185+
};
186+
187+
final payload = {
188+
'recipient_user_id': recipientUserId,
189+
'sessions_init': sessionsInit,
190+
};
191+
192+
final res = await dio.post(
193+
'$nodeUrl/sessions',
194+
data: payload,
195+
options: Options(headers: headers),
196+
);
197+
198+
if (res.statusCode == 200 || res.statusCode == 201) {
199+
return true;
200+
} else {
201+
debugPrint('CreateSessionFull failed http: ${res.statusCode} ${res.data}');
202+
return false;
203+
}
204+
} catch (e, st) {
205+
debugPrint('createSessionFull error: $e');
206+
debugPrintStack(stackTrace: st);
207+
return false;
208+
}
209+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import 'package:dio/dio.dart';
2+
import 'package:hushnet_frontend/models/users.dart';
3+
4+
Future<List<User>> fetchUsers(String nodeUrl) async {
5+
final dio = Dio();
6+
final res = await dio.get('$nodeUrl/users');
7+
final List<dynamic> data = res.data;
8+
return data.map((userJson) => User.fromJson(userJson)).toList();
9+
}

lib/models/pending_sessions.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
class PendingSession {
2+
final String id;
3+
final String senderDeviceId;
4+
final String recipientDeviceId;
5+
final String ephemeralPubkey; // base64
6+
final String ciphertext; // base64
7+
final String? senderPrekeyPub; // base64 (nécessaire pour DH1)
8+
final String createdAt;
9+
10+
PendingSession({
11+
required this.id,
12+
required this.senderDeviceId,
13+
required this.recipientDeviceId,
14+
required this.ephemeralPubkey,
15+
required this.ciphertext,
16+
required this.createdAt,
17+
this.senderPrekeyPub,
18+
});
19+
20+
factory PendingSession.fromJson(Map<String, dynamic> json) {
21+
return PendingSession(
22+
id: json['id'],
23+
senderDeviceId: json['sender_device_id'],
24+
recipientDeviceId: json['recipient_device_id'],
25+
ephemeralPubkey: json['ephemeral_pubkey'],
26+
ciphertext: json['ciphertext'],
27+
createdAt: json['created_at'] ?? '',
28+
senderPrekeyPub: json['sender_prekey_pub'],
29+
);
30+
}
31+
}

lib/models/user_device.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import 'dart:convert';
2+
3+
class UserDevice {
4+
final String deviceId;
5+
final String identityPubkey;
6+
final String prekeyPubkey;
7+
final String signedPrekeyPub;
8+
final String signedPrekeySig;
9+
final String? oneTimePrekeyPub;
10+
11+
const UserDevice({
12+
required this.deviceId,
13+
required this.prekeyPubkey,
14+
required this.identityPubkey,
15+
required this.signedPrekeyPub,
16+
required this.signedPrekeySig,
17+
this.oneTimePrekeyPub,
18+
});
19+
20+
factory UserDevice.fromJson(Map<String, dynamic> json) {
21+
return UserDevice(
22+
deviceId: json['id'] ?? json['device_id'],
23+
identityPubkey: json['identity_pubkey'],
24+
prekeyPubkey: json['prekey_pubkey'] ?? json['prekey']?['key'],
25+
signedPrekeyPub: json['signed_prekey_pub'] ?? json['signed_prekey']?['key'],
26+
signedPrekeySig: json['signed_prekey_sig'] ?? json['signed_prekey']?['signature'],
27+
oneTimePrekeyPub: json['one_time_prekeys'][0] ?? json['one_time_prekey']?['key'],
28+
);
29+
}
30+
31+
Map<String, dynamic> toJson() => {
32+
'device_id': deviceId,
33+
'prekey_pubkey': prekeyPubkey,
34+
'identity_pubkey': identityPubkey,
35+
'signed_prekey_pub': signedPrekeyPub,
36+
'signed_prekey_sig': signedPrekeySig,
37+
'one_time_prekey_pub': oneTimePrekeyPub,
38+
};
39+
40+
@override
41+
String toString() => jsonEncode(toJson());
42+
}

lib/models/users.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
class User {
2+
final String id;
3+
final String username;
4+
5+
User({
6+
required this.id,
7+
required this.username,
8+
});
9+
10+
factory User.fromJson(Map<String, dynamic> json) {
11+
return User(
12+
id: json['id'],
13+
username: json['username'],
14+
);
15+
}
16+
17+
}

0 commit comments

Comments
 (0)