✨ E2EE and Keypair
This commit is contained in:
@ -1,18 +1,97 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:solian/models/message.dart';
|
||||
import 'package:solian/providers/keypair.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
class ChatMessageContent extends StatelessWidget {
|
||||
class ChatMessageContent extends StatefulWidget {
|
||||
final Message item;
|
||||
|
||||
const ChatMessageContent({super.key, required this.item});
|
||||
|
||||
@override
|
||||
State<ChatMessageContent> createState() => _ChatMessageContentState();
|
||||
}
|
||||
|
||||
class _ChatMessageContentState extends State<ChatMessageContent> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (item.type == 'm.text') {
|
||||
final feColor = Theme.of(context).colorScheme.onSurface.withOpacity(0.65);
|
||||
|
||||
final waitingKeyHint = Row(
|
||||
children: [
|
||||
Icon(Icons.key, color: feColor, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.chatMessageUnableDecryptWaiting,
|
||||
style: TextStyle(color: feColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
final missingKeyHint = Row(
|
||||
children: [
|
||||
Icon(Icons.key_off_outlined, color: feColor, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.chatMessageUnableDecryptMissing,
|
||||
style: TextStyle(color: feColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (widget.item.type == 'm.text') {
|
||||
String? content;
|
||||
switch (widget.item.decodedContent['algorithm']) {
|
||||
case 'plain':
|
||||
content = widget.item.decodedContent['value'];
|
||||
case 'aes':
|
||||
final keypair = context.watch<KeypairProvider>();
|
||||
if (keypair.keys[widget.item.decodedContent['keypair_id']] == null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (keypair.requestKey(
|
||||
widget.item.decodedContent['keypair_id'],
|
||||
widget.item.decodedContent['algorithm'],
|
||||
widget.item.sender.account.externalId!,
|
||||
)) {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
content = keypair.decodeViaAESKey(
|
||||
widget.item.decodedContent['keypair_id'],
|
||||
widget.item.decodedContent['value'],
|
||||
)!;
|
||||
break;
|
||||
}
|
||||
|
||||
if (keypair.requestingKeys.contains(widget.item.decodedContent['keypair_id'])) {
|
||||
return waitingKeyHint.animate().swap(builder: (context, _) {
|
||||
return missingKeyHint;
|
||||
}, delay: 3000.ms);
|
||||
}
|
||||
}
|
||||
|
||||
if (content == null) {
|
||||
return Row(
|
||||
children: [
|
||||
Icon(Icons.key_off, color: feColor, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
AppLocalizations.of(context)!.chatMessageUnableDecryptUnsupported,
|
||||
style: TextStyle(color: feColor),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Markdown(
|
||||
data: item.decodedContent['value'],
|
||||
data: content,
|
||||
shrinkWrap: true,
|
||||
selectable: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
|
@ -9,6 +9,7 @@ import 'package:provider/provider.dart';
|
||||
import 'package:solian/models/message.dart';
|
||||
import 'package:solian/models/post.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/keypair.dart';
|
||||
import 'package:solian/utils/services_url.dart';
|
||||
import 'package:solian/widgets/exts.dart';
|
||||
import 'package:solian/widgets/posts/attachment_editor.dart';
|
||||
@ -19,11 +20,13 @@ class ChatMessageEditor extends StatefulWidget {
|
||||
final String realm;
|
||||
final Message? editing;
|
||||
final Message? replying;
|
||||
final bool isEncrypted;
|
||||
final Function? onReset;
|
||||
|
||||
const ChatMessageEditor({
|
||||
super.key,
|
||||
required this.channel,
|
||||
required this.isEncrypted,
|
||||
this.realm = 'global',
|
||||
this.editing,
|
||||
this.replying,
|
||||
@ -55,8 +58,19 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> buildContentBody(String content, {String algorithm = 'plain'}) {
|
||||
return {'value': content, 'algorithm': algorithm};
|
||||
Map<String, dynamic> buildContentBody(String content) {
|
||||
final keypair = context.read<KeypairProvider>();
|
||||
if (keypair.activeKeyId == null || keypair.keys[keypair.activeKeyId] == null) {
|
||||
final kp = keypair.generateAESKey();
|
||||
keypair.setActiveKey(kp.id);
|
||||
return buildContentBody(content);
|
||||
}
|
||||
|
||||
return {
|
||||
'value': widget.isEncrypted ? keypair.encodeViaAESKey(keypair.activeKeyId!, content) : content,
|
||||
'keypair_id': widget.isEncrypted ? keypair.activeKeyId : null,
|
||||
'algorithm': widget.isEncrypted ? 'aes' : 'plain',
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> sendMessage(BuildContext context) async {
|
||||
@ -214,7 +228,9 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> {
|
||||
autocorrect: true,
|
||||
keyboardType: TextInputType.text,
|
||||
decoration: InputDecoration.collapsed(
|
||||
hintText: AppLocalizations.of(context)!.chatMessagePlaceholder,
|
||||
hintText: widget.isEncrypted
|
||||
? AppLocalizations.of(context)!.chatMessageEncryptedPlaceholder
|
||||
: AppLocalizations.of(context)!.chatMessagePlaceholder,
|
||||
),
|
||||
onSubmitted: (_) => sendMessage(context),
|
||||
onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
|
||||
|
@ -1,63 +1,12 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:solian/models/packet.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/providers/keypair.dart';
|
||||
import 'package:solian/providers/notify.dart';
|
||||
import 'package:solian/router.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:solian/models/notification.dart' as model;
|
||||
import 'package:badges/badges.dart' as badge;
|
||||
|
||||
class NotificationNotifier extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const NotificationNotifier({super.key, required this.child});
|
||||
|
||||
@override
|
||||
State<NotificationNotifier> createState() => _NotificationNotifierState();
|
||||
}
|
||||
|
||||
class _NotificationNotifierState extends State<NotificationNotifier> {
|
||||
void connect() async {
|
||||
final notify = ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.connectingServer),
|
||||
duration: const Duration(minutes: 1),
|
||||
),
|
||||
);
|
||||
|
||||
final auth = context.read<AuthProvider>();
|
||||
final nty = context.read<NotifyProvider>();
|
||||
|
||||
if (await auth.isAuthorized()) {
|
||||
if (auth.client == null) {
|
||||
await auth.loadClient();
|
||||
}
|
||||
|
||||
nty.fetch(auth);
|
||||
nty.connect(auth);
|
||||
}
|
||||
|
||||
notify.close();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
Future.delayed(Duration.zero, () {
|
||||
connect();
|
||||
});
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationButton extends StatefulWidget {
|
||||
const NotificationButton({super.key});
|
||||
|
||||
@ -66,6 +15,43 @@ class NotificationButton extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _NotificationButtonState extends State<NotificationButton> {
|
||||
void connect() async {
|
||||
final auth = context.read<AuthProvider>();
|
||||
final nty = context.read<NotifyProvider>();
|
||||
final keypair = context.read<KeypairProvider>();
|
||||
|
||||
if (nty.isOpened) return;
|
||||
|
||||
final notify = ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.connectingServer),
|
||||
duration: const Duration(minutes: 1),
|
||||
),
|
||||
);
|
||||
|
||||
if (await auth.isAuthorized()) {
|
||||
if (auth.client == null) {
|
||||
await auth.loadClient();
|
||||
}
|
||||
|
||||
nty.fetch(auth);
|
||||
keypair.channel = await nty.connect(
|
||||
auth,
|
||||
onKexRequest: keypair.provideKeypair,
|
||||
onKexProvide: keypair.receiveKeypair,
|
||||
);
|
||||
}
|
||||
|
||||
notify.close();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
Future.delayed(Duration.zero, () => connect());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final nty = context.watch<NotifyProvider>();
|
||||
|
Reference in New Issue
Block a user