refactor: remove legacy tauri stack and update iced docs
This commit is contained in:
+33
-718
@@ -1,729 +1,44 @@
|
||||
# Software Design Document (SDD)
|
||||
# ReTeamSpeak - Cross-Platform TeamSpeak Client
|
||||
# Software Design Document
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Date**: 2026-05-12
|
||||
**Status**: Based on actual implementation
|
||||
## iced-app
|
||||
|
||||
---
|
||||
### `main.rs`
|
||||
|
||||
## 1. Detailed Design
|
||||
- Defines the full app state and message enum
|
||||
- Uses `SyncConnection` / `SyncConnectionHandle` from `tsclientlib`
|
||||
- Maintains local UI mirrors for:
|
||||
- bookmarks
|
||||
- server metadata
|
||||
- channels
|
||||
- clients
|
||||
- chat messages
|
||||
- device selection
|
||||
- mute / AFK state
|
||||
|
||||
### 1.1 `shared` Module Design
|
||||
### `audio.rs`
|
||||
|
||||
#### 1.1.1 Core Type System
|
||||
- Output playback through `cpal`
|
||||
- Input capture through `cpal`
|
||||
- Opus encode for outgoing voice
|
||||
- Simple VAD for continuous talk mode
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Type Hierarchy │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Identifier Types (Newtype Pattern) │
|
||||
│ ├── ClientId(u16) // In-session client ID │
|
||||
│ ├── ChannelId(u64) // Channel identifier │
|
||||
│ ├── ServerGroupId(u64) // Server group │
|
||||
│ ├── ChannelGroupId(u64) // Channel group │
|
||||
│ ├── ClientDbId(u64) // Database client ID │
|
||||
│ ├── Uid(String) // Unique identity (base64) │
|
||||
│ ├── PermissionId(u32) // Permission ID │
|
||||
│ └── IconId(i32) // Icon identifier │
|
||||
│ │
|
||||
│ Enumerations │
|
||||
│ ├── Codec { SpeexNB, SpeexWB, SpeexUWB, Celt, OpusVoice, │
|
||||
│ │ OpusMusic } │
|
||||
│ ├── ChannelType { Permanent, SemiPermanent, Temporary } │
|
||||
│ ├── ClientType { Normal, Query { admin } } │
|
||||
│ ├── ConnectionState { Disconnected, Connecting, ... } │
|
||||
│ ├── Reason { None, Moved, LostConnection, KickChannel, ... }│
|
||||
│ ├── CodecEncryptionMode { PerChannel, ForcedOff, ForcedOn }│
|
||||
│ ├── HostMessageMode { None, Log, Modal, Modalquit } │
|
||||
│ ├── GroupType { Template, Regular, Query } │
|
||||
│ └── GroupNamingMode { None, Before, After } │
|
||||
│ │
|
||||
│ Data Structures │
|
||||
│ ├── ServerInfo { id, name, platform, version, max_clients, │
|
||||
│ │ clients_online, ... } │
|
||||
│ ├── ChannelInfo { id, parent_id, name, codec, max_clients, │
|
||||
│ │ channel_type, ... } │
|
||||
│ ├── ClientInfo { id, channel_id, uid, name, muted, ... } │
|
||||
│ ├── ChatMessage { id, timestamp, invoker, target, message }│
|
||||
│ └── AppConfig { nickname, audio, hotkeys, theme, ... } │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
### `noise_cancel.rs`
|
||||
|
||||
#### 1.1.2 Event System Design
|
||||
- Runtime-selected noise cancellation backend
|
||||
- Current backends:
|
||||
- `None`
|
||||
- `nnnoiseless`
|
||||
- `sonora`
|
||||
|
||||
```
|
||||
AppEvent
|
||||
├── Connection(ConnectionEvent)
|
||||
│ ├── Connecting { address }
|
||||
│ ├── Connected { server, own_client }
|
||||
│ ├── StateChanged { state }
|
||||
│ ├── DisconnectedTemporarily { reason }
|
||||
│ ├── Disconnected { reason }
|
||||
│ └── ConnectionFailed { error }
|
||||
│
|
||||
├── Client(ClientEvent)
|
||||
│ ├── EnteredView { client, reason }
|
||||
│ ├── LeftView { client_id, reason, reason_message }
|
||||
│ ├── Updated { client_id, changes: ClientChanges }
|
||||
│ ├── Moved { client_id, from_channel, to_channel, reason }
|
||||
│ ├── StartedTalking { client_id }
|
||||
│ ├── StoppedTalking { client_id }
|
||||
│ ├── ServerGroupChanged { client_id, group_id, added }
|
||||
│ └── ChannelGroupChanged { client_id, group_id }
|
||||
│
|
||||
├── Channel(ChannelEvent)
|
||||
│ ├── Created { channel }
|
||||
│ ├── Deleted { channel_id }
|
||||
│ ├── Updated { channel_id, changes: ChannelChanges }
|
||||
│ ├── Moved { channel_id, new_parent, new_order }
|
||||
│ ├── PasswordChanged { channel_id }
|
||||
│ ├── DescriptionChanged { channel_id }
|
||||
│ └── Subscribed { channel_id, subscribed }
|
||||
│
|
||||
├── Server(ServerEvent)
|
||||
│ ├── Updated { changes: ServerChanges }
|
||||
│ ├── ServerGroupList { groups }
|
||||
│ └── ChannelGroupList { groups }
|
||||
│
|
||||
├── Message(MessageEvent)
|
||||
│ ├── Received { message }
|
||||
│ ├── Sent { message }
|
||||
│ ├── Read { message_id }
|
||||
│ └── UnreadCountChanged { count }
|
||||
│
|
||||
├── Audio(AudioEvent)
|
||||
│ ├── InputDeviceChanged { device }
|
||||
│ ├── OutputDeviceChanged { device }
|
||||
│ ├── InputVolumeChanged { volume }
|
||||
│ ├── OutputVolumeChanged { volume }
|
||||
│ ├── InputMutedChanged { muted }
|
||||
│ ├── OutputMutedChanged { muted }
|
||||
│ ├── DeviceList { input_devices, output_devices }
|
||||
│ ├── InputLevel { level }
|
||||
│ └── OutputLevel { level }
|
||||
│
|
||||
├── FileTransfer(FileTransferEvent)
|
||||
│ ├── Started { transfer_id, file_name, file_size, is_upload }
|
||||
│ ├── Progress { transfer_id, progress }
|
||||
│ ├── Completed { transfer_id }
|
||||
│ ├── Failed { transfer_id, error }
|
||||
│ └── Cancelled { transfer_id }
|
||||
│
|
||||
└── Error(ErrorEvent)
|
||||
├── Protocol { code, message }
|
||||
├── Network { message }
|
||||
├── Audio { message }
|
||||
├── Database { message }
|
||||
└── Other { message }
|
||||
```
|
||||
### `identity.rs`
|
||||
|
||||
---
|
||||
- Locates TeamSpeak config database in common Linux paths
|
||||
- Extracts `identity_secret_key`
|
||||
- Parses it into `tsclientlib::Identity`
|
||||
|
||||
### 1.2 `tscore` Module Design
|
||||
## Known Design Limitations
|
||||
|
||||
#### 1.2.1 Packet Processing Pipeline
|
||||
|
||||
```
|
||||
SEND RECEIVE
|
||||
┌──────────────────────────────┐ ┌──────────────────────────────┐
|
||||
│ Command String │ │ UDP Packet │
|
||||
│ │ │ │ │ │
|
||||
│ ▼ │ │ ▼ │
|
||||
│ Command::serialize() │ │ InPacket::parse() │
|
||||
│ │ │ │ │ │
|
||||
│ ▼ │ │ ▼ │
|
||||
│ QuickLZ compress │ │ AES-EAX decrypt │
|
||||
│ (if Command/CommandLow) │ │ (or fake decrypt) │
|
||||
│ │ │ │ │ │
|
||||
│ ▼ │ │ ▼ │
|
||||
│ Fragment (if > 500 bytes) │ │ QuickLZ decompress │
|
||||
│ │ │ │ (if COMPRESSED flag) │
|
||||
│ ▼ │ │ │ │
|
||||
│ AES-EAX encrypt │ │ ▼ │
|
||||
│ (or fake encrypt) │ │ Defragment │
|
||||
│ │ │ │ (if FRAGMENTED flag) │
|
||||
│ ▼ │ │ │ │
|
||||
│ Assign Packet ID │ │ ▼ │
|
||||
│ │ │ │ Command::parse() │
|
||||
│ ▼ │ │ │ │
|
||||
│ UDP Send │ │ ▼ │
|
||||
└──────────────────────────────┘ │ Application Layer │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 1.2.2 Encryption Key Derivation
|
||||
|
||||
```
|
||||
Input: packet_type, direction, generation_id, shared_iv[64]
|
||||
│
|
||||
▼
|
||||
┌───────────────────────────────────────────────────────┐
|
||||
│ temp[0] = direction_byte (0x30=S2C, 0x31=C2S) │
|
||||
│ temp[1] = packet_type.u8() │
|
||||
│ temp[2..6] = generation_id.to_be_bytes() │
|
||||
│ temp[6..70] = shared_iv[0..64] │
|
||||
│ │
|
||||
│ key_nonce = SHA-256(temp) │
|
||||
│ key = key_nonce[0..16] │
|
||||
│ nonce = key_nonce[16..32] │
|
||||
│ │
|
||||
│ key[0] ^= (packet_id >> 8) as u8 │
|
||||
│ key[1] ^= (packet_id & 0xFF) as u8 │
|
||||
└───────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
Output: key[16], nonce[16] → AES-128-EAX
|
||||
```
|
||||
|
||||
#### 1.2.3 Connection State Machine
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ Disconnected │
|
||||
└──────┬───────┘
|
||||
│ start_handshake()
|
||||
▼
|
||||
┌──────────────┐
|
||||
┌──────│ Connecting │◄─────────────────┐
|
||||
│ └──────┬───────┘ │
|
||||
│ │ Init1 received │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ IdentityLevelIncreasing│ │
|
||||
│ └──────┬───────────────┘ │
|
||||
│ │ Init3 received │
|
||||
│ ▼ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ Connected │──────────────────┤
|
||||
│ └──────┬───────┘ │
|
||||
│ │ channellistfinished │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────┐ │
|
||||
│ │ ChannelListFinished │ │
|
||||
│ └──────┬──────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────┐ │
|
||||
└─────>│ DisconnectedTemporarily │──────┘
|
||||
└──────┬───────────────────┘
|
||||
│ timeout / manual
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ Error │
|
||||
└──────┬───────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ Disconnected │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
#### 1.2.4 RSA Puzzle Solver
|
||||
|
||||
```rust
|
||||
/// Solves y = x^(2^level) mod n
|
||||
///
|
||||
/// Algorithm:
|
||||
/// y = x
|
||||
/// for i in 0..level:
|
||||
/// y = (y * y) mod n
|
||||
///
|
||||
/// Time complexity: O(level * M(n)) where M(n) is multiplication cost
|
||||
/// Space complexity: O(n) for big integer storage
|
||||
fn solve_rsa_puzzle(x: &[u8; 64], n: &[u8; 64], level: u32) -> [u8; 64] {
|
||||
let x_big = BigUint::from_bytes_be(x);
|
||||
let n_big = BigUint::from_bytes_be(n);
|
||||
let mut y = x_big;
|
||||
for _ in 0..level {
|
||||
y = (y.clone() * y) % &n_big;
|
||||
}
|
||||
// Convert back to 64-byte array (big-endian, zero-padded)
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2.5 Retransmission System
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ ResendManager │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ pending: BTreeMap<PacketId, SentPacket> │
|
||||
│ max_retries: u32 (default: 10) │
|
||||
│ connection_timeout: Duration (default: 30s) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ add_sent(id, data) → Add to pending queue │
|
||||
│ ack(id) → bool → Remove from pending │
|
||||
│ get_retransmissions() → Vec<(id, data)> to resend │
|
||||
│ is_connection_timeout()→ Check for dead connection │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ SentPacket: │
|
||||
│ data: Vec<u8> │
|
||||
│ sent_at: Instant │
|
||||
│ retry_count: u32 │
|
||||
│ timeout: Duration (starts at 500ms, doubles) │
|
||||
│ │
|
||||
│ RttEstimator: │
|
||||
│ srtt: Duration (smoothed RTT) │
|
||||
│ rtt_var: Duration (RTT variance) │
|
||||
│ rto: Duration (retransmission timeout) │
|
||||
│ update(measured_rtt) → recalculate SRTT, RTO │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.3 `tsaudio` Module Design
|
||||
|
||||
#### 1.3.1 Audio Pipeline
|
||||
|
||||
```
|
||||
CAPTURE PIPELINE:
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ cpal │───>│ VAD │───>│ Opus │───>│ Packet │
|
||||
│ capture │ │ detect │ │ encode │ │ output │
|
||||
└──────────┘ └──────────┘ └──────────┘ └──────────┘
|
||||
|
||||
PLAYBACK PIPELINE:
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ Packet │───>│ Jitter │───>│ Opus │───>│ cpal │
|
||||
│ input │ │ buffer │ │ decode │ │ playback │
|
||||
└──────────┘ └──────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
#### 1.3.2 Voice Activity Detection
|
||||
|
||||
```rust
|
||||
pub struct VadDetector {
|
||||
threshold: f32,
|
||||
state: VadState,
|
||||
}
|
||||
|
||||
impl VadDetector {
|
||||
pub fn detect(&mut self, samples: &[f32]) -> VadState {
|
||||
let energy = samples.iter().map(|s| s * s).sum::<f32>()
|
||||
/ samples.len() as f32;
|
||||
if energy > self.threshold {
|
||||
self.state = VadState::Speaking;
|
||||
} else {
|
||||
self.state = VadState::Silent;
|
||||
}
|
||||
self.state
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3.3 Jitter Buffer
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ JitterBuffer │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ buffer: Vec<Option<AudioFrame>> (ring buffer) │
|
||||
│ head: usize │
|
||||
│ tail: usize │
|
||||
│ size: usize │
|
||||
│ capacity: usize │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ push(frame) → Result<()> // Add frame │
|
||||
│ pop() → Option<AudioFrame> // Get next frame │
|
||||
│ len() → usize // Current buffer size │
|
||||
│ is_empty() → bool │
|
||||
│ is_full() → bool │
|
||||
│ clear() // Reset buffer │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.4 `tsdb` Module Design
|
||||
|
||||
#### 1.4.1 Database Schema
|
||||
|
||||
```sql
|
||||
-- Identity storage
|
||||
CREATE TABLE identities (
|
||||
id TEXT PRIMARY KEY, -- UUID
|
||||
name TEXT NOT NULL, -- Display name
|
||||
private_key TEXT NOT NULL, -- Base64 encoded ECC private key
|
||||
counter INTEGER DEFAULT 0, -- Hash Cash counter
|
||||
max_counter INTEGER DEFAULT 0, -- Maximum counter tried
|
||||
created_at TEXT NOT NULL, -- ISO 8601 timestamp
|
||||
updated_at TEXT NOT NULL -- ISO 8601 timestamp
|
||||
);
|
||||
|
||||
-- Server bookmarks
|
||||
CREATE TABLE bookmarks (
|
||||
id TEXT PRIMARY KEY, -- UUID
|
||||
name TEXT NOT NULL, -- Display name
|
||||
address TEXT NOT NULL, -- Server address
|
||||
port INTEGER DEFAULT 9987, -- Server port
|
||||
nickname TEXT, -- Preferred nickname
|
||||
server_password TEXT, -- Encrypted server password
|
||||
channel TEXT, -- Default channel
|
||||
channel_password TEXT, -- Encrypted channel password
|
||||
default_token TEXT, -- Permission token
|
||||
auto_connect INTEGER DEFAULT 0,-- Auto-connect on startup
|
||||
last_connected TEXT, -- Last connection timestamp
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Message history
|
||||
CREATE TABLE messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
server_address TEXT NOT NULL, -- Server address
|
||||
invoker_id INTEGER NOT NULL, -- Client ID
|
||||
invoker_name TEXT NOT NULL, -- Display name
|
||||
invoker_uid TEXT NOT NULL, -- Unique ID
|
||||
target_type TEXT NOT NULL, -- "server", "channel", "client"
|
||||
target_id INTEGER, -- Target ID
|
||||
message TEXT NOT NULL, -- Message content
|
||||
is_read INTEGER DEFAULT 0, -- Read status
|
||||
timestamp TEXT NOT NULL -- ISO 8601 timestamp
|
||||
);
|
||||
|
||||
-- Key-value settings
|
||||
CREATE TABLE settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
#### 1.4.2 CRUD Operations
|
||||
|
||||
```rust
|
||||
impl DatabaseManager {
|
||||
// Identity operations
|
||||
fn create_identity(&self, name, private_key) -> DatabaseResult<Identity>;
|
||||
fn get_identity(&self, id) -> DatabaseResult<Identity>;
|
||||
fn get_all_identities(&self) -> DatabaseResult<Vec<Identity>>;
|
||||
fn update_identity(&self, id, name?, counter?) -> DatabaseResult<()>;
|
||||
fn delete_identity(&self, id) -> DatabaseResult<()>;
|
||||
|
||||
// Bookmark operations
|
||||
fn create_bookmark(&self, name, address, port, nickname?) -> DatabaseResult<Bookmark>;
|
||||
fn get_bookmark(&self, id) -> DatabaseResult<Bookmark>;
|
||||
fn get_all_bookmarks(&self) -> DatabaseResult<Vec<Bookmark>>;
|
||||
fn update_bookmark(&self, id, name?, address?, port?, nickname?) -> DatabaseResult<()>;
|
||||
fn delete_bookmark(&self, id) -> DatabaseResult<()>;
|
||||
|
||||
// Message operations
|
||||
fn create_message(&self, server_address, invoker_id, invoker_name,
|
||||
invoker_uid, target_type, target_id?, message) -> DatabaseResult<Message>;
|
||||
fn get_server_messages(&self, server_address, limit, offset) -> DatabaseResult<Vec<Message>>;
|
||||
fn mark_message_read(&self, id) -> DatabaseResult<()>;
|
||||
|
||||
// Settings operations
|
||||
fn get_setting(&self, key) -> DatabaseResult<Option<String>>;
|
||||
fn set_setting(&self, key, value) -> DatabaseResult<()>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.5 `tauri-app` Design
|
||||
|
||||
#### 1.5.1 Tauri Command Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Frontend (React) │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ invoke("get_bookmarks") → Promise<BookmarkInfo[]> │ │
|
||||
│ │ invoke("connect", {addr, port, nick, pass}) │ │
|
||||
│ │ invoke("send_message", {target, message}) │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│ Tauri IPC
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Tauri Shell (Rust) │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ #[tauri::command] │ │
|
||||
│ │ async fn get_bookmarks(state: State<AppState>) │ │
|
||||
│ │ -> Result<Vec<BookmarkInfo>, String> │ │
|
||||
│ │ { │ │
|
||||
│ │ state.db.get_all_bookmarks() │ │
|
||||
│ │ .map(|b| b.into_iter().map(Into::into)) │ │
|
||||
│ │ .map_err(|e| e.to_string()) │ │
|
||||
│ │ } │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ AppState { │ │
|
||||
│ │ db: DatabaseManager, │ │
|
||||
│ │ connection_state: Mutex<ConnectionState>, │ │
|
||||
│ │ } │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 1.5.2 React Component Structure
|
||||
|
||||
```
|
||||
App
|
||||
├── Header
|
||||
│ ├── Logo
|
||||
│ ├── ConnectionStatus
|
||||
│ └── SettingsButton
|
||||
│
|
||||
├── Sidebar
|
||||
│ ├── BookmarkList
|
||||
│ │ └── BookmarkItem (clickable)
|
||||
│ └── RecentServers
|
||||
│
|
||||
├── MainContent
|
||||
│ ├── ConnectForm (when no connection)
|
||||
│ │ ├── AddressInput
|
||||
│ │ ├── NicknameInput
|
||||
│ │ ├── PasswordInput
|
||||
│ │ └── ConnectButton
|
||||
│ │
|
||||
│ └── ChatView (when connected)
|
||||
│ ├── ChannelTree
|
||||
│ ├── ClientList
|
||||
│ ├── MessageList
|
||||
│ └── MessageInput
|
||||
│
|
||||
└── StatusBar
|
||||
├── ConnectionInfo
|
||||
├── AudioStatus
|
||||
└── LatencyDisplay
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Algorithm Specifications
|
||||
|
||||
### 2.1 Shared Secret Computation (Old Protocol <3.1)
|
||||
|
||||
```
|
||||
Input: alpha[10], beta[10], shared_data[32]
|
||||
Output: SharedIV[64], SharedMac[8]
|
||||
|
||||
1. SharedIV[0..20] = SHA-1(shared_data)
|
||||
2. SharedIV[0..10] ^= alpha[0..10]
|
||||
3. SharedIV[10..20] ^= beta[0..10]
|
||||
4. SharedMac[0..8] = SHA-1(SharedIV)[0..8]
|
||||
```
|
||||
|
||||
### 2.2 Shared Secret Computation (New Protocol ≥3.1)
|
||||
|
||||
```
|
||||
Input: alpha[10], beta[54], shared_data[32]
|
||||
Output: SharedIV[64], SharedMac[8]
|
||||
|
||||
1. SharedIV[0..64] = SHA-512(shared_data)
|
||||
2. SharedIV[0..10] ^= alpha[0..10]
|
||||
3. SharedIV[10..64] ^= beta[0..54]
|
||||
4. SharedMac[0..8] = SHA-1(SharedIV)[0..8]
|
||||
```
|
||||
|
||||
### 2.3 Hash Cash Level Computation
|
||||
|
||||
```
|
||||
Input: omega (public key string), offset (u64)
|
||||
Output: level (u8)
|
||||
|
||||
1. data = SHA-1(omega + offset.to_string())
|
||||
2. level = 0
|
||||
3. for byte in data:
|
||||
4. if byte == 0:
|
||||
5. level += 8
|
||||
6. else:
|
||||
7. level += trailing_zeros(byte)
|
||||
8. break
|
||||
9. return level
|
||||
```
|
||||
|
||||
### 2.4 UID Computation
|
||||
|
||||
```
|
||||
Input: publicKey (ASN.1-DER encoded)
|
||||
Output: uid (base64 string)
|
||||
|
||||
1. hash = SHA-1(publicKey)
|
||||
2. uid = base64(hash)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Interface Specifications
|
||||
|
||||
### 3.1 Tauri IPC Interface
|
||||
|
||||
```typescript
|
||||
// TypeScript interface for Tauri commands
|
||||
interface ITauriCommands {
|
||||
// Identity management
|
||||
get_identities(): Promise<IdentityInfo[]>;
|
||||
create_identity(name: string): Promise<IdentityInfo>;
|
||||
delete_identity(id: string): Promise<void>;
|
||||
|
||||
// Bookmark management
|
||||
get_bookmarks(): Promise<BookmarkInfo[]>;
|
||||
create_bookmark(name: string, address: string, port: number,
|
||||
nickname?: string): Promise<BookmarkInfo>;
|
||||
delete_bookmark(id: string): Promise<void>;
|
||||
|
||||
// Connection
|
||||
connect(address: string, port: number, nickname: string,
|
||||
password?: string): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
|
||||
// Messaging
|
||||
send_message(target: string, message: string): Promise<void>;
|
||||
get_messages(server_address: string, limit: number,
|
||||
offset: number): Promise<MessageInfo[]>;
|
||||
}
|
||||
|
||||
interface IdentityInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
counter: number;
|
||||
max_counter: number;
|
||||
}
|
||||
|
||||
interface BookmarkInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
address: string;
|
||||
port: number;
|
||||
nickname: string | null;
|
||||
auto_connect: boolean;
|
||||
last_connected: string | null;
|
||||
}
|
||||
|
||||
interface MessageInfo {
|
||||
id: number;
|
||||
invoker_name: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
is_read: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Internal Rust Interfaces
|
||||
|
||||
```rust
|
||||
// Protocol layer
|
||||
pub trait PacketProcessor {
|
||||
fn encode(&self, packet: OutPacket) -> Result<Vec<OutUdpPacket>>;
|
||||
fn decode(&self, data: &[u8]) -> Result<InPacket>;
|
||||
}
|
||||
|
||||
// Audio layer
|
||||
pub trait AudioCapture {
|
||||
async fn start(&mut self) -> AudioResult<()>;
|
||||
async fn stop(&mut self) -> AudioResult<()>;
|
||||
async fn capture(&mut self) -> AudioResult<AudioFrame>;
|
||||
}
|
||||
|
||||
pub trait AudioPlayback {
|
||||
async fn start(&mut self) -> AudioResult<()>;
|
||||
async fn stop(&mut self) -> AudioResult<()>;
|
||||
async fn play(&mut self, frame: AudioFrame) -> AudioResult<()>;
|
||||
}
|
||||
|
||||
// Database layer
|
||||
pub trait IdentityStore {
|
||||
fn create(&self, name: &str, key: &str) -> DatabaseResult<Identity>;
|
||||
fn get(&self, id: &str) -> DatabaseResult<Identity>;
|
||||
fn list(&self) -> DatabaseResult<Vec<Identity>>;
|
||||
fn update(&self, id: &str, updates: IdentityUpdates) -> DatabaseResult<()>;
|
||||
fn delete(&self, id: &str) -> DatabaseResult<()>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Data Dictionary
|
||||
|
||||
### 4.1 Protocol Fields
|
||||
|
||||
| Field | Type | Size | Description |
|
||||
|-------|------|------|-------------|
|
||||
| MAC | [u8; 8] | 8 bytes | EAX message authentication code |
|
||||
| PId | u16 | 2 bytes | Packet sequence ID |
|
||||
| CId | u16 | 2 bytes | Client ID (C2S only) |
|
||||
| PT | u8 | 1 byte | Packet type + flags |
|
||||
| VId | u16 | 2 bytes | Voice packet ID |
|
||||
| Codec | u8 | 1 byte | Audio codec type |
|
||||
|
||||
### 4.2 Flag Bits
|
||||
|
||||
| Bit | Name | Mask | Description |
|
||||
|-----|------|------|-------------|
|
||||
| 7 | UE | 0x80 | Unencrypted |
|
||||
| 6 | CP | 0x40 | Compressed (QuickLZ) |
|
||||
| 5 | NP | 0x20 | New protocol |
|
||||
| 4 | FR | 0x10 | Fragmented |
|
||||
| 3-0 | Type | 0x0F | Packet type (0-8) |
|
||||
|
||||
### 4.3 Error Codes
|
||||
|
||||
| Code | Name | Description |
|
||||
|------|------|-------------|
|
||||
| 0x0000 | ok | Success |
|
||||
| 0x0200 | client_invalid_id | Invalid client ID |
|
||||
| 0x0201 | client_nickname_inuse | Nickname already in use |
|
||||
| 0x0208 | client_invalid_password | Wrong password |
|
||||
| 0x0300 | channel_invalid_id | Invalid channel ID |
|
||||
| 0x0400 | server_invalid_id | Invalid server ID |
|
||||
| 0x0403 | server_maxclients_reached | Server full |
|
||||
| 0x0701 | connection_lost | Connection lost |
|
||||
|
||||
---
|
||||
|
||||
## 5. Test Design
|
||||
|
||||
### 5.1 Test Cases (32 total)
|
||||
|
||||
#### Protocol Tests (14)
|
||||
1. `test_packet_type_conversion` - PacketType enum conversion
|
||||
2. `test_flags` - Flag bit manipulation
|
||||
3. `test_header_c2s` - C2S header parsing
|
||||
4. `test_header_s2c` - S2C header parsing
|
||||
5. `test_in_packet_parse` - Input packet parsing
|
||||
6. `test_out_packet` - Output packet creation
|
||||
7. `test_command_parse` - Command string parsing
|
||||
8. `test_command_serialize` - Command serialization
|
||||
9. `test_command_builder` - CommandBuilder pattern
|
||||
10. `test_escape_sequences` - Escape/unescape
|
||||
11. `test_init_packet_parse` - Init packet parsing
|
||||
12. `test_init_packet_serialize` - Init packet serialization
|
||||
13. `test_ack_packet` - Acknowledgement packet
|
||||
14. `test_packet_type_properties` - Type property queries
|
||||
|
||||
#### Crypto Tests (12)
|
||||
1. `test_sha1` - SHA-1 hash
|
||||
2. `test_sha256` - SHA-256 hash
|
||||
3. `test_sha512` - SHA-512 hash
|
||||
4. `test_hash_password` - Password hashing
|
||||
5. `test_create_key_nonce` - Key derivation
|
||||
6. `test_create_encryption_key` - Packet-specific key
|
||||
7. `test_shared_secret_old` - Old protocol shared secret
|
||||
8. `test_shared_secret_new` - New protocol shared secret
|
||||
9. `test_key_cache` - Key caching
|
||||
10. `test_eax_encrypt_decrypt` - EAX encryption/decryption
|
||||
11. `test_fake_encrypt_decrypt` - Fake encryption
|
||||
12. `test_hash_cash_level` - Hash Cash computation
|
||||
13. `test_compute_uid` - UID computation
|
||||
|
||||
#### Connection Tests (6)
|
||||
1. `test_encode_version` - Version encoding
|
||||
2. `test_rsa_puzzle` - RSA puzzle solver
|
||||
3. `test_resend_manager` - Retransmission manager
|
||||
4. `test_rtt_estimator` - RTT estimation
|
||||
5. `test_sent_packet_retry` - Packet retry logic
|
||||
|
||||
---
|
||||
|
||||
## 6. References
|
||||
|
||||
1. TS3 Protocol Paper: `refercence/tsdeclarations/ts3protocol.md`
|
||||
2. Packet Definitions: `refercence/tsdeclarations/Packets.txt`
|
||||
3. Message Definitions: `refercence/tsdeclarations/Messages.toml`
|
||||
4. Source Code: `src/` (4780 lines, 39 Rust files)
|
||||
- `main.rs` is still monolithic and should be split later
|
||||
- Bookmarks are not persisted
|
||||
- ServerQuery is not wired to a real backend yet
|
||||
- Audio settings apply to runtime state, but broader config persistence is not implemented
|
||||
|
||||
Reference in New Issue
Block a user