Skip to content

Commit 1742eec

Browse files
committed
MVP done
1 parent ac6eac3 commit 1742eec

24 files changed

Lines changed: 1446 additions & 1668 deletions

.vscode/launch.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
55
"version": "0.2.0",
66
"configurations": [
7+
78
{
89
"name": "airchat",
910
"request": "launch",

android/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
2222

2323
<application
24-
android:icon="@mipmap/launcher_icon"
24+
android:icon="@mipmap/ic_launcher"
2525
android:label="AirChat"
2626
android:usesCleartextTraffic="true">
2727
<activity

lib/providers/connection_state_provider.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,16 @@ class ConnectionStateProvider extends ChangeNotifier {
66
final Map<String, String> discovered = {}; // userId -> name
77
String? inChatUserId;
88
bool discovering = false;
9+
AppLifecycleState appState = AppLifecycleState.resumed;
910

1011
// Use LAN backend's connected peers
1112
List<LanPeer> get connectedPeers => LanConnectionService().connectedPeers;
1213

14+
void setAppState(AppLifecycleState state){
15+
appState = state;
16+
notifyListeners();
17+
}
18+
1319
void setInChatUserId(String? userId) {
1420
inChatUserId = userId;
1521
notifyListeners();

lib/services/base_connection_service.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ abstract class BaseConnectionService {
44
void stopService();
55

66
Stream<List<dynamic>> get discoveredPeersStream;
7-
Stream<Map<String, dynamic>> get messageStream;
8-
Stream<Map<String, dynamic>?> get fileTransferProgressStream;
7+
Stream<Map<String, dynamic>> get messageEventStream;
8+
Stream<Map<String, dynamic>> get fileEventStream;
9+
Stream<Map<String, dynamic>> get fileTransferProgressStream;
910

1011
Future<void> sendMessage(String id, dynamic peer, String message, String? type);
1112
Future<void> sendFile(String id, dynamic peer, String filePath, {String? fileName});

lib/services/connection_service.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ class ConnectionService {
118118
static Stream<Map<String, dynamic>> get fileEventsStream =>
119119
_lan.fileEventStream;
120120

121-
static Stream<Map<String, dynamic>?> get fileTransferProgressStream =>
121+
static Stream<Map<String, dynamic>> get fileTransferProgressStream =>
122122
_lan.fileTransferProgressStream;
123123

124124
// Discovery/Advertising (LAN: only discovery is needed)

lib/services/encryption_service.dart

Lines changed: 57 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -60,21 +60,22 @@ class EncryptionService {
6060
}
6161

6262
/// Encrypt a message (text or file bytes)
63-
Future<Map<String, dynamic>> encrypt(Uint8List plainBytes) async {
63+
Future<Map<String, dynamic>> encrypt(Uint8List plainBytes,
64+
{bool inNewThread = false}) async {
6465
if (_sharedAesKey == null) throw Exception('AES key not derived');
6566

6667
final aes = AesGcm.with256bits();
6768

68-
// Future<SecretBox> encryptInIsolate() async {
69-
// return await aes.encrypt(
70-
// plainBytes,
71-
// secretKey: _sharedAesKey!,
72-
// );
73-
// }
74-
75-
final encrypted = await aes.encrypt(
69+
Future<SecretBox> encryptInIsolate() async {
70+
return await aes.encrypt(
7671
plainBytes,
77-
secretKey: _sharedAesKey!);
72+
secretKey: _sharedAesKey!,
73+
);
74+
}
75+
76+
final encrypted = inNewThread
77+
? await Isolate.run(() => encryptInIsolate())
78+
: await aes.encrypt(plainBytes, secretKey: _sharedAesKey!);
7879

7980
return {
8081
'nonce': base64Encode(encrypted.nonce),
@@ -85,79 +86,81 @@ class EncryptionService {
8586

8687
/// Encrypt a stream message (text or file bytes) and return a stream of encrypted bytes.
8788
/// The returned stream emits the encrypted bytes as they become available.
88-
/// The nonce and mac are returned alongside the stream for decryption.
89+
/// The nonce is returned immediately, and the MAC is provided via the [onMac] callback when available.
8990
///
9091
/// Usage:
91-
/// final result = await encryptStream(plainBytes);
92-
/// result['stream'] is the Stream`<List<int>>`of encrypted bytes.
93-
/// result['nonce'] and result['mac'] are base64 strings.
94-
Future<Map<String, dynamic>> encryptStream(
95-
Stream<List<int>> plainBytes) async {
92+
/// final stream = encryptStream(
93+
/// plainBytes,
94+
/// onMac: (mac, nonce) {
95+
/// // Use mac and nonce when available
96+
/// },
97+
/// );
98+
/// // stream is Stream`<List<int>>`
99+
Stream<List<int>> encryptStream(
100+
Stream<List<int>> plainBytes, {
101+
required void Function(String mac, String nonce) onMac,
102+
}) {
96103
if (_sharedAesKey == null) throw Exception('AES key not derived');
97104

98105
final aes = AesGcm.with256bits();
99106
final nonce = aes.newNonce();
100107

101-
// Prepare a controller to output the encrypted bytes
102-
final controller = StreamController<List<int>>();
103-
Mac? mac;
104-
105-
// Start encryption
106108
final secretBoxStream = aes.encryptStream(
107109
plainBytes,
108110
secretKey: _sharedAesKey!,
109111
nonce: nonce,
110112
onMac: (Mac m) {
111-
mac = m;
113+
onMac(base64Encode(m.bytes), base64Encode(nonce));
112114
},
113115
);
114116

115-
// Listen to the SecretBox stream and add cipherText chunks to the output stream
116-
secretBoxStream.listen(
117-
(secretBox) {
118-
controller.add(secretBox);
119-
},
120-
onError: controller.addError,
121-
onDone: () async {
122-
await controller.close();
123-
},
124-
cancelOnError: true,
125-
);
117+
// The stream emits List<int> (cipherText chunks)
118+
return secretBoxStream;
119+
}
120+
121+
/// Decrypt a received message
122+
Future<List<int>> decrypt(
123+
{required String nonce,
124+
required List<int> cipherText,
125+
required String mac,
126+
bool isNewThread = false}) async {
127+
if (_sharedAesKey == null) throw Exception('AES key not derived');
126128

127-
// Wait for the first chunk to ensure the stream is valid
128-
await controller.done;
129+
final aes = AesGcm.with256bits();
130+
final box = SecretBox(
131+
cipherText,
132+
nonce: base64Decode(nonce),
133+
mac: Mac(base64Decode(mac)),
134+
);
129135

130-
if (mac == null) {
131-
throw Exception('Encryption failed: MAC not generated');
136+
Future<List<int>> decryptInIsolate() async {
137+
return await aes.decrypt(box, secretKey: _sharedAesKey!);
132138
}
133139

134-
return {
135-
'nonce': base64Encode(nonce),
136-
'mac': base64Encode(mac!.bytes),
137-
'stream': controller.stream,
138-
};
140+
return isNewThread
141+
? await Isolate.run(() => decryptInIsolate())
142+
: await aes.decrypt(box, secretKey: _sharedAesKey!);
139143
}
140144

141-
/// Decrypt a received message
142-
Future<List<int>> decrypt({
145+
/// Decrypts a stream of encrypted data chunks (cipherText) using the current shared AES key.
146+
///
147+
/// [cipherTextStream] is a stream of encrypted bytes (cipherText).
148+
/// [nonce] and [mac] are required for decryption and should be provided as base64 strings.
149+
/// Returns a stream of decrypted bytes.
150+
Stream<List<int>> decryptStream({
151+
required Stream<List<int>> cipherTextStream,
143152
required String nonce,
144-
required List<int> cipherText,
145153
required String mac,
146-
}) async {
154+
}) {
147155
if (_sharedAesKey == null) throw Exception('AES key not derived');
148156

149157
final aes = AesGcm.with256bits();
150-
final box = SecretBox(
151-
cipherText,
158+
return aes.decryptStream(
159+
cipherTextStream,
160+
secretKey: _sharedAesKey!,
152161
nonce: base64Decode(nonce),
153162
mac: Mac(base64Decode(mac)),
154163
);
155-
156-
// Future<List<int>> decryptInIsolate() async {
157-
// return await aes.decrypt(box, secretKey: _sharedAesKey!);
158-
// }
159-
160-
return await aes.decrypt(box, secretKey: _sharedAesKey!);
161164
}
162165

163166
/// Reset state (for testing or re-handshake)

0 commit comments

Comments
 (0)