Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ app.*.map.json
# FVM Version Cache
.fvm/

.vscode/
.vscode/
android/.kotlin/
1 change: 1 addition & 0 deletions ios/Flutter/Debug.xcconfig
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
1 change: 1 addition & 0 deletions ios/Flutter/Release.xcconfig
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
43 changes: 43 additions & 0 deletions ios/Podfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}

def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end

File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end

require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

flutter_ios_podfile_setup

target 'Runner' do
use_frameworks!

flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end

post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
30 changes: 22 additions & 8 deletions lib/data/node/sessions/create_session.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,17 @@ import 'package:flutter/material.dart';
import 'package:hushnet_frontend/services/key_provider.dart';
import 'package:hushnet_frontend/services/node_service.dart';

/// Full X3DH + initial AES-GCM encrypt for each recipient device, then POST /sessions
Future<bool> createSession(String nodeUrl, String recipientUserId) async {
/// Full X3DH + initial AES-GCM encrypt for each recipient device, then POST /sessions.
///
/// Pass [recipientUserAddress] (e.g. "bob@node-b.hushnet.net") for federated
/// users. The local node proxies key fetching and session forwarding.
/// [recipientUserId] is ignored server-side for remote recipients; pass the
/// nil UUID as a placeholder when you only have the federated address.
Future<bool> createSession(
String nodeUrl,
String recipientUserId, {
String? recipientUserAddress,
}) async {
final keyProvider = KeyProvider();
final dio = Dio();
final NodeService nodeService = NodeService();
Expand All @@ -22,8 +31,11 @@ Future<bool> createSession(String nodeUrl, String recipientUserId) async {
throw Exception('Missing identity or prekey on client');
}

// 2) get recipient devices (assumes these return X25519 pubs)
final devices = await keyProvider.getUserDevicesKeys(recipientUserId);
// 2) get recipient devices — federated path uses the proxy endpoint
final devices = recipientUserAddress != null
? await keyProvider.getRemoteUserDevicesKeys(recipientUserAddress)
: await keyProvider.getUserDevicesKeys(recipientUserId);

if (devices.isEmpty) {
debugPrint('No devices for recipient');
return false;
Expand Down Expand Up @@ -192,7 +204,7 @@ Future<bool> createSession(String nodeUrl, String recipientUserId) async {
final nonce = aes.newNonce();
final plaintext = utf8.encode(
'HushNet initial session message',
); // customize initial message as needed
);
final secretBox = await aes.encrypt(
plaintext,
secretKey: rootKey,
Expand All @@ -211,9 +223,8 @@ Future<bool> createSession(String nodeUrl, String recipientUserId) async {
sessionsInit.add({
'recipient_device_id': device.deviceId,
'ephemeral_pubkey': ekPubB64,
'sender_identity_pub': base64Encode(ikPub),
'ciphertext': ciphertextB64,
'otpk_used': device.oneTimePrekeyPub,
'otpk_used': device.oneTimePrekeyPub ?? '',
'sender_prekey_pub': base64Encode(ikPub),
});
} // end for devices
Expand Down Expand Up @@ -245,6 +256,8 @@ Future<bool> createSession(String nodeUrl, String recipientUserId) async {

final payload = {
'recipient_user_id': recipientUserId,
if (recipientUserAddress != null)
'recipient_user_address': recipientUserAddress,
'sessions_init': sessionsInit,
};

Expand All @@ -254,7 +267,8 @@ Future<bool> createSession(String nodeUrl, String recipientUserId) async {
options: Options(headers: headers),
);

if (res.statusCode == 200 || res.statusCode == 201) {
// 202 means the session was forwarded to the remote node — treat as success
if (res.statusCode == 200 || res.statusCode == 201 || res.statusCode == 202) {
return true;
} else {
debugPrint('CreateSessionFull failed http: ${res.statusCode} ${res.data}');
Expand Down
1 change: 0 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ class MyHomePage extends StatefulWidget {
}

class _MyHomePageState extends State<MyHomePage> {

@override
Widget build(BuildContext context) {
return const OnboardingScreen();
Expand Down
24 changes: 22 additions & 2 deletions lib/models/chat_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ class ChatView {
final String? lastMessagePreview;
final DateTime updatedAt;
final String displayName;
// Populated by server for federated chats (partner_federated_address).
final String? federatedAddress;

ChatView({
required this.id,
Expand All @@ -21,8 +23,11 @@ class ChatView {
this.lastMessagePreview,
required this.updatedAt,
this.displayName = '',
this.federatedAddress,
});

bool get isRemote => federatedAddress != null;

factory ChatView.fromJson(Map<String, dynamic> json) {
return ChatView(
id: json['id'] ?? '',
Expand All @@ -34,6 +39,22 @@ class ChatView {
lastMessagePreview: json['last_message_preview'],
updatedAt: DateTime.parse(json['updated_at']),
displayName: json['name'] ?? json['partner_username'] ?? '',
federatedAddress: json['partner_federated_address'],
);
}

ChatView copyWith({String? federatedAddress}) {
return ChatView(
id: id,
chatType: chatType,
partnerUserId: partnerUserId,
partnerUsername: partnerUsername,
name: name,
lastMessageId: lastMessageId,
lastMessagePreview: lastMessagePreview,
updatedAt: updatedAt,
displayName: displayName,
federatedAddress: federatedAddress ?? this.federatedAddress,
);
}

Expand All @@ -47,15 +68,14 @@ class ChatView {
'last_message_id': lastMessageId,
'last_message_preview': lastMessagePreview,
'updated_at': updatedAt.toIso8601String(),
if (federatedAddress != null) 'partner_federated_address': federatedAddress,
};
}

/// Optional helper: format the last update nicely for UI
String get formattedDate {
return DateFormat('dd/MM HH:mm').format(updatedAt);
}

/// Optional helper: title shown in chat list
String get displayTitle {
if (chatType == 'group') return name ?? 'Group chat';
return partnerUsername ?? 'Unknown';
Expand Down
7 changes: 6 additions & 1 deletion lib/models/pending_sessions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ class PendingSession {
final String ephemeralPubkey; // base64
final String ciphertext; // base64
final String? senderPrekeyPub; // base64 (nécessaire pour DH1)
final String? otpkUsed; // base64 public key of the OPK Alice used
final String createdAt;
User? senderUser;

PendingSession({
required this.id,
required this.senderDeviceId,
Expand All @@ -19,6 +20,7 @@ class PendingSession {
required this.createdAt,
this.senderUser,
this.senderPrekeyPub,
this.otpkUsed,
});

factory PendingSession.fromJson(Map<String, dynamic> json) {
Expand All @@ -30,6 +32,9 @@ class PendingSession {
ciphertext: json['ciphertext'],
createdAt: json['created_at'] ?? '',
senderPrekeyPub: json['sender_prekey_pub'],
otpkUsed: json['otpk_used'] is String && (json['otpk_used'] as String).isNotEmpty
? json['otpk_used']
: null,
);
}
}
45 changes: 35 additions & 10 deletions lib/models/user_device.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,45 @@ class UserDevice {
deviceId: json['id'] ?? json['device_id'],
identityPubkey: json['identity_pubkey'],
prekeyPubkey: json['prekey_pubkey'] ?? json['prekey']?['key'],
signedPrekeyPub: json['signed_prekey_pub'] ?? json['signed_prekey']?['key'],
signedPrekeySig: json['signed_prekey_sig'] ?? json['signed_prekey']?['signature'],
oneTimePrekeyPub: json['one_time_prekeys'][0] ?? json['one_time_prekey']?['key'],
signedPrekeyPub:
json['signed_prekey_pub'] ?? json['signed_prekey']?['key'],
signedPrekeySig:
json['signed_prekey_sig'] ?? json['signed_prekey']?['signature'],
oneTimePrekeyPub:
json['one_time_prekeys'][0] ?? json['one_time_prekey']?['key'],
);
}

// Federated endpoint returns a slightly different shape:
// device_id, identity_pubkey, signed_prekey_pub, signed_prekey_sig,
// one_time_prekeys: ["base64", ...] (strings, not objects)
factory UserDevice.fromFederatedJson(Map<String, dynamic> json) {
final otpks = json['one_time_prekeys'];
String? otpk;
if (otpks is List && otpks.isNotEmpty) {
final first = otpks[0];
otpk = first is String
? first
: (first is Map ? first['key'] as String? : null);
}
return UserDevice(
deviceId: json['device_id'] ?? json['id'],
identityPubkey: json['identity_pubkey'],
prekeyPubkey: json['prekey_pubkey'],
signedPrekeyPub: json['signed_prekey_pub'],
signedPrekeySig: json['signed_prekey_sig'],
oneTimePrekeyPub: otpk,
);
}

Map<String, dynamic> toJson() => {
'device_id': deviceId,
'prekey_pubkey': prekeyPubkey,
'identity_pubkey': identityPubkey,
'signed_prekey_pub': signedPrekeyPub,
'signed_prekey_sig': signedPrekeySig,
'one_time_prekey_pub': oneTimePrekeyPub,
};
'device_id': deviceId,
'prekey_pubkey': prekeyPubkey,
'identity_pubkey': identityPubkey,
'signed_prekey_pub': signedPrekeyPub,
'signed_prekey_sig': signedPrekeySig,
'one_time_prekey_pub': oneTimePrekeyPub,
};

@override
String toString() => jsonEncode(toJson());
Expand Down
Loading
Loading