Butterfly
Butterfly copied to clipboard
[Enhancement]: Stabilize collaboration
Which feature is your request related to?
No response
Describe your request for enhancements!
Currently collaboration is experimental since a long time. We should finally stabilize it. Some things which should be added
- Add useful error messages
- Hide technical options inside an advanced tab
- Add username field
- Add option to configure a "Lobby" system to not allow everyone to join
- Add browse lan games
- Add qr code (and note that the person needs to be in the same network)
In the future maybe:
- Add relay server system
Additional context
No response
Code of Conduct
- [x] I agree to follow this project's Code of Conduct
For end to end encryption to easily allow relay server we could use these examples:
Example Dart code
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:cryptography/cryptography.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
/// Encrypts a message using AES-GCM.
/// The key is derived by hashing the secret (SHA-256) to produce a 256-bit key.
/// A random 12-byte nonce is generated for each message. The output is:
/// base64( nonce || ciphertext || mac )
Future<String> encryptMessage(String message, String secret) async {
// Derive a 256-bit key from the secret:
final keyBytes = utf8.encode(secret);
final hash = await Sha256().hash(keyBytes);
final secretKey = SecretKey(hash.bytes);
// AES-GCM with 256-bit key:
final algorithm = AesGcm.with256bits();
// Generate a random 12-byte nonce:
final nonce = _generateRandomNonce(12);
// Encrypt the message:
final secretBox = await algorithm.encrypt(
utf8.encode(message),
secretKey: secretKey,
nonce: nonce,
);
// Concatenate nonce, ciphertext, and MAC:
final combined = <int>[];
combined.addAll(nonce);
combined.addAll(secretBox.cipherText);
combined.addAll(secretBox.mac.bytes);
// Return as a Base64-encoded string.
return base64.encode(combined);
}
/// Decrypts a Base64-encoded string that contains nonce || ciphertext || mac.
Future<String> decryptMessage(String encryptedMessage, String secret) async {
// Decode from Base64:
final combined = base64.decode(encryptedMessage);
// AES-GCM typically uses a 12-byte nonce and 16-byte MAC:
const nonceLength = 12;
const macLength = 16;
if (combined.length < nonceLength + macLength) {
throw Exception('Invalid encrypted message');
}
final nonce = combined.sublist(0, nonceLength);
final macBytes = combined.sublist(combined.length - macLength);
final cipherText = combined.sublist(nonceLength, combined.length - macLength);
// Derive the same key from the secret:
final keyBytes = utf8.encode(secret);
final hash = await Sha256().hash(keyBytes);
final secretKey = SecretKey(hash.bytes);
final algorithm = AesGcm.with256bits();
final secretBox = SecretBox(
cipherText,
nonce: nonce,
mac: Mac(macBytes),
);
// Decrypt:
final clearTextBytes = await algorithm.decrypt(
secretBox,
secretKey: secretKey,
);
return utf8.decode(clearTextBytes);
}
/// Generates a list of random bytes of the specified [length].
List<int> _generateRandomNonce(int length) {
final random = Random.secure();
return List<int>.generate(length, (_) => random.nextInt(256));
}
/// ChatClient using WebSockets with end-to-end encryption.
class ChatClient {
final WebSocketChannel channel;
final String encryptionKey; // e.g. the secret extracted from the room URL
ChatClient(String url, this.encryptionKey)
: channel = WebSocketChannel.connect(Uri.parse(url)) {
// Listen for incoming messages.
channel.stream.listen((data) async {
try {
// Assume the incoming data is a Base64 string.
final decrypted = await decryptMessage(data, encryptionKey);
print("Received decrypted: $decrypted");
// Process the decrypted message (update UI, etc.)
} catch (e) {
print("Decryption error: $e");
}
}, onError: (error) {
print("Socket error: $error");
});
}
/// Encrypts the [message] and sends it over the WebSocket.
Future<void> sendMessage(String message) async {
try {
final encrypted = await encryptMessage(message, encryptionKey);
channel.sink.add(encrypted);
print("Sent encrypted: $encrypted");
} catch (e) {
print("Encryption error: $e");
}
}
void dispose() {
channel.sink.close();
}
}
void main() async {
// For example, your room URL might be:
// https://excalidraw.com/#room=968bc76e9d3fa44fbcbd,6TSIcavZwRpKdl7cNLL61w
// Extract the secret part ("6TSIcavZwRpKdl7cNLL61w") to use as the encryption key.
final roomWebSocketUrl = "wss://your-relay-server.example.com";
final secretKey = "6TSIcavZwRpKdl7cNLL61w";
final chatClient = ChatClient(roomWebSocketUrl, secretKey);
// Send an encrypted message:
await chatClient.sendMessage("Hello, end-to-end encrypted world!");
// The client remains active to receive messages.
}
For inspiration: Excalidraw: https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/data/encryption.ts
ID and secret can be inside the url with the relay server that will be used. When connecting we need to confirm the user if we really want to connect since we then communicate with the relay server.
moved to 2.5