E2EE and Keypair

This commit is contained in:
2024-05-12 20:15:12 +08:00
parent 08d0a99b10
commit 98547708af
20 changed files with 665 additions and 115 deletions

146
lib/providers/keypair.dart Normal file
View File

@ -0,0 +1,146 @@
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:solian/models/keypair.dart';
import 'package:solian/models/packet.dart';
import 'package:uuid/uuid.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
class KeypairProvider extends ChangeNotifier {
static const storage = FlutterSecureStorage();
static const encryptIV = 'WT7s~><Ae?YrJd)D';
WebSocketChannel? channel;
String? activeKeyId;
Map<String, Keypair> keys = {};
List<String> requestingKeys = List.empty(growable: true);
KeypairProvider() {
loadKeys();
}
void loadKeys() async {
final result = await storage.read(key: 'keypair');
if (result != null) {
jsonDecode(result).values.forEach((x) {
keys[x['id']] = Keypair.fromJson(x);
});
activeKeyId = await storage.read(key: 'keypairActive');
}
notifyListeners();
}
void saveKeys() async {
await storage.write(key: 'keypair', value: jsonEncode(keys));
if (activeKeyId != null) {
await storage.write(key: 'keypairActive', value: activeKeyId);
}
}
void receiveKeypair(Keypair kp) {
keys[kp.id] = kp;
requestingKeys.remove(kp.id);
saveKeys();
notifyListeners();
}
Keypair? provideKeypair(String id) {
print(id);
print(keys[id]);
return keys[id];
}
void importKeys(String code) {
final result = jsonDecode(utf8.fuse(base64).decode(code)).map((x) => Keypair.fromJson(x)).toList();
for (final item in result) {
if (item is Keypair) {
keys[item.id] = item;
}
}
saveKeys();
notifyListeners();
}
void setActiveKey(String id) {
if (keys[id] == null) return;
activeKeyId = id;
saveKeys();
notifyListeners();
}
void clearKeys() {
keys = {};
storage.delete(key: 'keypairActive');
saveKeys();
}
bool requestKey(String id, String algorithm, int uid) {
if (channel == null) return false;
if (requestingKeys.contains(id)) return false;
channel!.sink.add(jsonEncode(
NetworkPackage(method: 'kex.request', payload: {
'request_id': const Uuid().v4(),
'keypair_id': id,
'algorithm': algorithm,
'owner_id': uid,
'deadline': 3,
}).toJson(),
));
requestingKeys.add(id);
notifyListeners();
return true;
}
String? encodeViaAESKey(String keypairId, String content) {
if (keys[keypairId] == null) {
return null;
} else if (keys[keypairId]?.algorithm != 'aes') {
throw Exception('invalid algorithm');
}
final kp = keys[keypairId]!;
final iv = encrypt.IV.fromUtf8(encryptIV);
final key = encrypt.Key.fromBase64(kp.publicKey);
final encryptor = encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.sic, padding: null));
return encryptor.encryptBytes(utf8.encode(content), iv: iv).base64;
}
String? decodeViaAESKey(String keypairId, String encrypted) {
if (keys[keypairId] == null) {
return null;
} else if (keys[keypairId]?.algorithm != 'aes') {
throw Exception('invalid algorithm');
}
final kp = keys[keypairId]!;
final iv = encrypt.IV.fromUtf8(encryptIV);
final key = encrypt.Key.fromBase64(kp.publicKey);
final encryptor = encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.sic, padding: null));
return utf8.decode(encryptor.decryptBytes(encrypt.Encrypted.fromBase64(encrypted), iv: iv));
}
Keypair generateAESKey() {
final random = Random.secure();
final values = List<int>.generate(32, (i) => random.nextInt(256));
final key = Uint8List.fromList(values);
final kp = Keypair(
id: const Uuid().v4(),
algorithm: 'aes',
publicKey: base64.encode(key),
privateKey: null,
isOwned: true,
);
keys[kp.id] = kp;
return kp;
}
}

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:solian/models/keypair.dart';
import 'package:solian/models/packet.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart';
@ -63,9 +64,13 @@ class NotifyProvider extends ChangeNotifier {
notifyListeners();
}
Future<void> connect(AuthProvider auth) async {
Future<WebSocketChannel?> connect(
AuthProvider auth, {
Keypair? Function(String id)? onKexRequest,
Function(Keypair kp)? onKexProvide,
}) async {
if (auth.client == null) await auth.loadClient();
if (!await auth.isAuthorized()) return;
if (!await auth.isAuthorized()) return null;
await auth.client!.refreshToken(auth.client!.currentRefreshToken!);
@ -87,19 +92,40 @@ class NotifyProvider extends ChangeNotifier {
switch (result.method) {
case 'notifications.new':
final result = model.Notification.fromJson(jsonDecode(event));
onRemoteMessage(result);
unreadAmount++;
notifications.add(result);
notifyListeners();
notifyMessage(result.subject, result.content);
break;
case 'kex.request':
if (onKexRequest == null || result.payload == null) break;
final resp = onKexRequest(result.payload!['keypair_id']);
if (resp == null) break;
channel.sink.add(jsonEncode(
NetworkPackage(method: 'kex.provide', payload: {
'request_id': result.payload!['request_id'],
'keypair_id': resp.id,
'public_key': resp.publicKey,
'algorithm': resp.algorithm,
}).toJson(),
));
break;
case 'kex.provide':
if (onKexProvide == null || result.payload == null) break;
onKexProvide(Keypair(
id: result.payload!['keypair_id'],
algorithm: result.payload?['algorithm'] ?? 'aes',
publicKey: result.payload!['public_key'],
privateKey: result.payload?['private_key'],
));
break;
}
},
onError: (_, __) => connect(auth),
onDone: () => connect(auth),
);
}
void onRemoteMessage(model.Notification item) {
unreadAmount++;
notifications.add(item);
notifyListeners();
return channel;
}
void notifyMessage(String title, String body) {
@ -136,7 +162,7 @@ class NotifyProvider extends ChangeNotifier {
notifyListeners();
}
void clearRealtime() {
void clearRealtimeNotifications() {
notifications = notifications.where((x) => !x.isRealtime).toList();
notifyListeners();
}