Files
Rightup ae68112377 - Refactored ACL authentication flow with shared helpers for replay detection and session updates.
- Changed blank-password login behavior to create/manage guest sessions (when read-only is enabled), including replay protection and session timestamp tracking.
- Preserved and centralized `sync_since` handling during successful auth/session updates.
- Updated login handling to return explicit `(success, permissions)` and reset room-server sync guard state in SQLite on successful room-server login.
- Updated identity/banner output in mesh CLI from `pyMC_*` to `openHop_*` and adjusted default version fallback.
- Extended path handling to support encoded path-length semantics via `PathUtils`, with legacy fallback support for older/malformed packets.
- Added bundled ACK extraction from PATH payload extras and callback-based ACK propagation.
- Hardened room-server push logic: max-failure early skip, corrected expected ACK CRC calculation, path encoding compatibility, persisted sync/last-activity fields, and passed expected CRC + timeout into packet injection.
- Enhanced packet router ACK flow with dispatcher ACK registration helper, support for multipart ACK wrapper handling, configurable ACK wait timeout/CRC in injection, and ensured PATH helper processing always runs.
- Updated daemon wiring to pass ACK callback into `PathHelper`,
- make dispatcher dedupe configurable (prevent dbl deduping), and only register duplicate logging hook when dedupe is enabled.
- Expanded tests to cover guest blank-password replay tracking, updated room-server injector ACK expectations, and new duplicate-logging-hook gating behavior.
2026-07-02 16:32:32 +01:00

201 lines
7.3 KiB
Python

import logging
import time
from typing import Dict, Optional
from openhop_core.protocol import Identity
from openhop_core.protocol.constants import PUB_KEY_SIZE
logger = logging.getLogger("ACL")
PERM_ACL_GUEST = 0x01
PERM_ACL_ADMIN = 0x02
PERM_ACL_READ_WRITE = 0x01
PERM_ACL_ROLE_MASK = 0x03
class ClientInfo:
"""Represents an authenticated client in the access control list."""
def __init__(self, identity: Identity, permissions: int = 0):
self.id = identity
self.permissions = permissions
self.shared_secret = b""
self.last_timestamp = 0
self.last_activity = 0
self.last_login_success = 0
self.out_path_len = -1
self.out_path = bytearray()
self.sync_since = 0 # For room servers - timestamp of last synced message
def is_admin(self) -> bool:
return (self.permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN
def is_guest(self) -> bool:
return (self.permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_GUEST
class ACL:
def __init__(
self,
max_clients: int = 50,
admin_password: Optional[str] = None,
guest_password: Optional[str] = None,
allow_read_only: bool = True,
):
self.max_clients = max_clients
self.admin_password = admin_password or ""
self.guest_password = guest_password or ""
self.allow_read_only = allow_read_only
self.clients: Dict[bytes, ClientInfo] = {}
def _is_replay(self, client: ClientInfo, timestamp: int) -> bool:
if timestamp <= client.last_timestamp:
logger.warning(
f"Possible replay attack! timestamp={timestamp}, last={client.last_timestamp}"
)
return True
return False
def _touch_client_session(
self,
client: ClientInfo,
shared_secret: bytes,
timestamp: int,
sync_since: int = None,
) -> None:
now = int(time.time())
client.last_timestamp = timestamp
client.last_activity = now
client.last_login_success = now
client.shared_secret = shared_secret
if sync_since is not None:
client.sync_since = sync_since
logger.debug(f"Stored sync_since={sync_since} for client")
def authenticate_client(
self,
client_identity: Identity,
shared_secret: bytes,
password: str,
timestamp: int,
sync_since: int = None,
target_identity_hash: int = None,
target_identity_name: str = None,
target_identity_config: dict = None,
) -> tuple[bool, int]:
target_identity_config = target_identity_config or {}
# Check for identity-specific passwords (required for room servers)
identity_settings = target_identity_config.get("settings", {})
# Determine if this is a room server by checking the type field
identity_type = target_identity_config.get("type", "")
is_room_server = identity_type == "room_server"
# Log sync_since if provided (room server format)
if sync_since is not None:
logger.debug(f"Client sync_since timestamp: {sync_since}")
if is_room_server:
# Room servers use passwords from their settings section only
# Empty strings are treated as "not set"
admin_pwd = identity_settings.get("admin_password") or None
guest_pwd = identity_settings.get("guest_password") or None
if not admin_pwd and not guest_pwd:
logger.error(
f"Room server '{target_identity_name}' has no passwords configured! Set admin_password and/or guest_password in settings."
)
return False, 0
else:
# Repeater uses global passwords from its own security section
admin_pwd = self.admin_password
guest_pwd = self.guest_password
logger.debug(
f"Repeater passwords - admin: {'SET' if admin_pwd else 'NONE'}, "
f"guest: {'SET' if guest_pwd else 'NONE'}"
)
admin_pwd = admin_pwd or ""
guest_pwd = guest_pwd or ""
if target_identity_name:
logger.debug(
f"Authenticating for identity '{target_identity_name}' (room_server={is_room_server})"
)
pub_key = client_identity.get_public_key()[:PUB_KEY_SIZE]
if not password:
client = self.clients.get(pub_key)
if client is None:
if not self.allow_read_only:
logger.info("Blank password, sender not in ACL and read-only disabled")
return False, 0
if len(self.clients) >= self.max_clients:
logger.warning("ACL full, cannot add client")
return False, 0
client = ClientInfo(client_identity, PERM_ACL_GUEST)
self.clients[pub_key] = client
logger.info("Blank password, allowing read-only guest access")
else:
logger.info(f"ACL-based login for {pub_key[:6].hex()}...")
if self._is_replay(client, timestamp):
return False, 0
self._touch_client_session(client, shared_secret, timestamp, sync_since=sync_since)
if (client.permissions & PERM_ACL_ROLE_MASK) == 0:
client.permissions |= PERM_ACL_GUEST
return True, client.permissions
permissions = 0
logger.debug(f"Comparing password (len={len(password)}) against admin/guest")
logger.debug(
f"Admin pwd len={len(admin_pwd) if admin_pwd else 0}, Guest pwd len={len(guest_pwd) if guest_pwd else 0}"
)
if admin_pwd and password == admin_pwd:
permissions = PERM_ACL_ADMIN
logger.info(f"Admin password validated for '{target_identity_name or 'unknown'}'")
elif guest_pwd and password == guest_pwd:
permissions = PERM_ACL_READ_WRITE
logger.info(f"Guest password validated for '{target_identity_name or 'unknown'}'")
else:
logger.info(f"Invalid password for '{target_identity_name or 'unknown'}'")
return False, 0
client = self.clients.get(pub_key)
if client is None:
if len(self.clients) >= self.max_clients:
logger.warning("ACL full, cannot add client")
return False, 0
client = ClientInfo(client_identity, 0)
self.clients[pub_key] = client
logger.info(f"Added new client {pub_key[:6].hex()}...")
if self._is_replay(client, timestamp):
return False, 0
self._touch_client_session(client, shared_secret, timestamp, sync_since=sync_since)
client.permissions &= ~PERM_ACL_ROLE_MASK
client.permissions |= permissions
logger.info(f"Login success! Permissions: {'ADMIN' if client.is_admin() else 'GUEST'}")
return True, client.permissions
def get_client(self, pub_key: bytes) -> Optional[ClientInfo]:
return self.clients.get(pub_key[:PUB_KEY_SIZE])
def get_num_clients(self) -> int:
return len(self.clients)
def get_all_clients(self):
return list(self.clients.values())
def remove_client(self, pub_key: bytes) -> bool:
key = pub_key[:PUB_KEY_SIZE]
if key in self.clients:
del self.clients[key]
return True
return False