mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-15 07:04:26 +10:00
2763d83fe4
At the top of the channel chat screen is an icon, indicating the channel type. Previously, the public icon was used correctly, but the hashtag icon was used for all other types. Now, consistent with the channels screen, we use the lock icon for private channels, and the composite icons for community public & community hashtag types. The fix for private channels was trivial, as we can identify hashtag channels by their name. Finding out whether a channel belongs to a community is much more involved. All the hard-working code was copied from channels_screen.dart. (I tried refactoring to reduce duplication, but my results were complex and not worth it.) Closes #432
150 lines
4.8 KiB
Dart
150 lines
4.8 KiB
Dart
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:crypto/crypto.dart' as crypto;
|
|
|
|
import '../connector/meshcore_protocol.dart';
|
|
import 'community.dart';
|
|
|
|
enum ChannelType { public, private, hashtag, communityPublic, communityHashtag }
|
|
|
|
class Channel {
|
|
final int index;
|
|
final String name;
|
|
final Uint8List psk; // 16 bytes
|
|
int unreadCount;
|
|
|
|
Channel({
|
|
required this.index,
|
|
required this.name,
|
|
required this.psk,
|
|
this.unreadCount = 0,
|
|
});
|
|
|
|
String get pskHex => _bytesToHex(psk);
|
|
|
|
bool get isEmpty => name.isEmpty && psk.every((b) => b == 0);
|
|
|
|
bool get isPublicChannel => pskHex == publicChannelPsk;
|
|
|
|
static Channel? fromFrame(Uint8List frame) {
|
|
// CHANNEL_INFO format:
|
|
// [0] = RESP_CODE_CHANNEL_INFO (18)
|
|
// [1] = channel_idx
|
|
// [2-33] = name (32 bytes, null-terminated)
|
|
// [34-49] = psk (16 bytes)
|
|
if (frame.length < 50) return null;
|
|
final reader = BufferReader(frame);
|
|
try {
|
|
if (reader.readByte() != respCodeChannelInfo) return null;
|
|
final index = reader.readByte();
|
|
final name = reader.readCStringGreedy(32);
|
|
final psk = reader.readBytes(16);
|
|
return Channel(index: index, name: name, psk: psk);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static Channel empty(int index) {
|
|
return Channel(index: index, name: '', psk: Uint8List(16));
|
|
}
|
|
|
|
static Channel fromHex(int index, String name, String pskHex) {
|
|
final psk = parsePskHex(pskHex);
|
|
return Channel(index: index, name: name, psk: psk);
|
|
}
|
|
|
|
static Uint8List parsePskHex(String hex) {
|
|
final cleaned = hex.replaceAll(RegExp(r'[^0-9a-fA-F]'), '');
|
|
if (cleaned.length != 32) {
|
|
throw const FormatException('PSK must be 32 hex characters');
|
|
}
|
|
final bytes = Uint8List(16);
|
|
for (int i = 0; i < 16; i++) {
|
|
final start = i * 2;
|
|
bytes[i] = int.parse(cleaned.substring(start, start + 2), radix: 16);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
/// Derive PSK from hashtag name using SHA256.
|
|
/// The hashtag is normalized to include '#' prefix.
|
|
/// Returns first 16 bytes of SHA256 hash as PSK.
|
|
static Uint8List derivePskFromHashtag(String hashtag) {
|
|
final name = hashtag.startsWith('#') ? hashtag : '#$hashtag';
|
|
final hash = crypto.sha256.convert(utf8.encode(name)).bytes;
|
|
return Uint8List.fromList(hash.sublist(0, 16));
|
|
}
|
|
|
|
/// Derive PSK for community public channel using HMAC-SHA256.
|
|
/// PSK = HMAC-SHA256(K, "channel:v1:__public__")[:16]
|
|
///
|
|
/// This creates a channel that is "public" only to members who have
|
|
/// the community secret. Outsiders see only opaque IDs.
|
|
static Uint8List deriveCommunityPublicPsk(Uint8List secret) {
|
|
final hmac = crypto.Hmac(crypto.sha256, secret);
|
|
final digest = hmac.convert(utf8.encode('channel:v1:__public__'));
|
|
return Uint8List.fromList(digest.bytes.sublist(0, 16));
|
|
}
|
|
|
|
/// Derive PSK for community hashtag channel using HMAC-SHA256.
|
|
/// PSK = HMAC-SHA256(K, "channel:v1:" + normalized_name)[:16]
|
|
///
|
|
/// Community hashtag channels are deterministic for all members
|
|
/// (same name => same id) but impossible to enumerate/guess without K.
|
|
static Uint8List deriveCommunityHashtagPsk(Uint8List secret, String hashtag) {
|
|
final normalized = _normalizeCommunityHashtag(hashtag);
|
|
final hmac = crypto.Hmac(crypto.sha256, secret);
|
|
final digest = hmac.convert(utf8.encode('channel:v1:$normalized'));
|
|
return Uint8List.fromList(digest.bytes.sublist(0, 16));
|
|
}
|
|
|
|
/// Normalize a hashtag name for consistent community PSK derivation.
|
|
/// Strips leading #, converts to lowercase, trims whitespace.
|
|
static String _normalizeCommunityHashtag(String hashtag) {
|
|
return hashtag.replaceFirst(RegExp(r'^#'), '').toLowerCase().trim();
|
|
}
|
|
|
|
static String formatPskHex(Uint8List psk) {
|
|
return _bytesToHex(psk);
|
|
}
|
|
|
|
static String _bytesToHex(Uint8List bytes) {
|
|
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
|
}
|
|
|
|
static bool isCommunityChannel(ChannelType channelType) {
|
|
switch (channelType) {
|
|
case ChannelType.communityPublic:
|
|
case ChannelType.communityHashtag:
|
|
return true;
|
|
case ChannelType.public:
|
|
case ChannelType.private:
|
|
case ChannelType.hashtag:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static ChannelType getChannelType(
|
|
Channel channel,
|
|
CommunityPskIndex communityIndex,
|
|
) {
|
|
Community? community = communityIndex.getCommunityForChannel(channel);
|
|
if (community != null) {
|
|
if (Community.isCommunityPublicChannel(channel, community)) {
|
|
return ChannelType.communityPublic;
|
|
}
|
|
return ChannelType.communityHashtag;
|
|
}
|
|
if (channel.isPublicChannel) {
|
|
return ChannelType.public;
|
|
} else if (channel.name.startsWith('#')) {
|
|
return ChannelType.hashtag;
|
|
}
|
|
return ChannelType.private;
|
|
}
|
|
|
|
static const String publicChannelPsk = '8b3387e9c5cdea6ac9e5edbaa115cd72';
|
|
}
|