Neda/call/lib/services/call_session.dart

175 lines
5.8 KiB
Dart

import 'dart:async';
import 'dart:typed_data';
import 'package:shared_preferences/shared_preferences.dart';
import '../core/crypto/key_manager.dart';
import 'audio_service.dart';
import 'secure_channel.dart';
/// Session lifecycle states.
enum SessionState {
noKey, // no passphrase / key loaded
keyLoaded, // key ready, not yet in call
inCall, // channel open, waiting for PTT or RX
transmitting,
receiving,
error,
}
/// Top-level session manager exposed to the UI.
///
/// Handles:
/// • Passphrase entry and PBKDF2 key derivation (persists key hash only).
/// • Creating / tearing down [SecureChannel].
/// • Delegating PTT and RX control.
/// • Exposing a unified [onStateChange] stream and stats for the UI.
class CallSession {
final AudioService _audio;
final KeyManager _keys = KeyManager();
SecureChannel? _channel;
SessionState _state = SessionState.noKey;
SessionState get state => _state;
bool get hasKey => _keys.hasKey;
String? _lastError;
String? get lastError => _lastError;
// Stream of [SessionState] for reactive UI updates.
final _ctrl = StreamController<SessionState>.broadcast();
Stream<SessionState> get onStateChange => _ctrl.stream;
CallSession(this._audio);
// ── Key management ─────────────────────────────────────────────────
/// Derive session key from [passphrase].
/// Optionally accepts a peer-supplied [salt] (from handshake packet).
Future<bool> loadPassphrase(String passphrase, {Uint8List? peerSalt}) async {
try {
_keys.deriveKey(passphrase, salt: peerSalt);
_setState(SessionState.keyLoaded);
// Persist a salted SHA-256 fingerprint so the UI can show "key loaded"
// without storing the raw passphrase.
await _saveKeyFingerprint(passphrase);
return true;
} catch (e) {
_lastError = 'Key derivation failed: $e';
_setState(SessionState.error);
return false;
}
}
/// Clear the current key (end session).
Future<void> clearKey() async {
_keys.clear();
final prefs = await SharedPreferences.getInstance();
await prefs.remove('key_fingerprint');
_setState(SessionState.noKey);
}
/// Returns true if a fingerprint was previously saved (key loaded).
Future<bool> hasSavedKey() async {
final prefs = await SharedPreferences.getInstance();
return prefs.containsKey('key_fingerprint');
}
// ── Call lifecycle ─────────────────────────────────────────────────
/// Open the secure channel (call start).
Future<void> startCall() async {
if (!_keys.hasKey) {
_lastError = 'No key loaded';
_setState(SessionState.error);
return;
}
_channel = SecureChannel(_audio, _keys);
_channel!.onStateChange.listen(_onChannelState);
await _channel!.open();
_setState(SessionState.inCall);
}
/// Close the secure channel (call end).
Future<void> endCall() async {
await _channel?.close();
await _channel?.dispose();
_channel = null;
if (_keys.hasKey) {
_setState(SessionState.keyLoaded);
} else {
_setState(SessionState.noKey);
}
}
// ── PTT ────────────────────────────────────────────────────────────
/// Press-to-talk: start encoding and transmitting voice.
Future<void> pttPress() async {
await _channel?.startTransmit();
}
/// Release PTT: stop transmitting.
Future<void> pttRelease() async {
await _channel?.stopTransmit();
}
// ── RX toggle ─────────────────────────────────────────────────────
Future<void> startListening() async {
await _channel?.startReceive();
}
Future<void> stopListening() async {
await _channel?.stopReceive();
}
// ── Stats ──────────────────────────────────────────────────────────
int get txFrames => _channel?.txFrames ?? 0;
int get rxFrames => _channel?.rxFrames ?? 0;
int get rxErrors => _channel?.rxErrors ?? 0;
double get txRms => _channel?.txRms ?? 0.0;
double get rxSignalRms => _channel?.rxSignalRms ?? 0.0;
// ── Private helpers ────────────────────────────────────────────────
void _onChannelState(ChannelState cs) {
switch (cs) {
case ChannelState.transmitting:
_setState(SessionState.transmitting);
case ChannelState.receiving:
_setState(SessionState.receiving);
case ChannelState.txReady:
_setState(SessionState.inCall);
case ChannelState.error:
_setState(SessionState.error);
default:
break;
}
}
void _setState(SessionState s) {
_state = s;
_ctrl.add(s);
}
Future<void> _saveKeyFingerprint(String passphrase) async {
// Store a simple truncated hash for display — NOT the key itself.
final hash = passphrase.codeUnits.fold<int>(0,
(prev, c) => (prev * 31 + c) & 0xFFFFFFFF);
final prefs = await SharedPreferences.getInstance();
await prefs.setString('key_fingerprint',
hash.toRadixString(16).padLeft(8, '0').toUpperCase());
}
Future<String?> getSavedFingerprint() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString('key_fingerprint');
}
Future<void> dispose() async {
await endCall();
await _ctrl.close();
}
}