mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
formats all dart files using `dart format .` from the root project dir this makes the code style repeatable by new contributors and makes PR review easier
106 lines
3.7 KiB
Dart
106 lines
3.7 KiB
Dart
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:crypto/crypto.dart' as crypto;
|
|
|
|
import '../connector/meshcore_protocol.dart';
|
|
|
|
class Channel {
|
|
final int index;
|
|
final String name;
|
|
final Uint8List psk; // 16 bytes
|
|
|
|
Channel({required this.index, required this.name, required this.psk});
|
|
|
|
String get pskHex => _bytesToHex(psk);
|
|
|
|
bool get isEmpty => name.isEmpty && psk.every((b) => b == 0);
|
|
|
|
bool get isPublicChannel => pskHex == publicChannelPsk;
|
|
|
|
static Channel? fromFrame(Uint8List data) {
|
|
// 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 (data.length < 50) return null;
|
|
if (data[0] != respCodeChannelInfo) return null;
|
|
|
|
final index = data[1];
|
|
final name = readCString(data, 2, 32);
|
|
final psk = Uint8List.fromList(data.sublist(34, 50));
|
|
|
|
return Channel(index: index, name: name, psk: psk);
|
|
}
|
|
|
|
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 const String publicChannelPsk = '8b3387e9c5cdea6ac9e5edbaa115cd72';
|
|
}
|