How Doom Handles Password Authentication: A Complete Technical Guide

Doom authenticates users by collecting credentials in the LoginScreen, passing them to LoadingScreen, and binding to an LDAP server using the get_ldap_connection function with automatic fallback from plaintext to TLS encryption.

Doom, an open-source penetration testing tool maintained in the 000pp/doom repository, implements a robust password authentication flow designed for enterprise environments. Understanding how Doom handles password authentication reveals a three-stage pipeline that bridges UI components with backend LDAP operations, supporting both cleartext passwords and legacy LM hashes.

The Three-Stage Authentication Pipeline

Doom's password authentication follows a clear separation of concerns across three distinct stages: UI collection, async credential handoff, and LDAP binding with protocol negotiation.

Stage 1: Credential Collection in LoginScreen

The authentication journey begins in src/doom/screens/login_screen.py, where the LoginScreen class renders a password input field with masking enabled. When the user clicks the Login button, the application extracts the raw password value from the UI component.


# From LoginScreen (lines 85-108)

password = self.query_one("#password-input").value

The password field is explicitly configured with password=True to ensure characters are masked during entry, protecting sensitive credentials from shoulder surfing.

Stage 2: Async Handoff to LoadingScreen

Once collected, the credentials—including the password—are packaged into a dictionary and passed to LoadingScreen via screen navigation. This transition occurs in src/doom/screens/login_screen.py (lines 98-112).

login_data = {
    'ip': ip,
    'domain': domain,
    'username': username,
    'password': password
}
loading_screen = LoadingScreen(login_data)
self.app.push_screen(loading_screen)

This handoff decouples the UI layer from the network authentication logic, allowing the application to display loading states while performing potentially slow LDAP operations.

Stage 3: LDAP Bind with Protocol Fallback

Upon mounting, LoadingScreen spawns an asynchronous task that invokes get_ldap_connection from src/doom/protocols/ldap.py. This function handles the actual password authentication against the directory server (LoadingScreen lines 71-84).

connection_result = await asyncio.to_thread(
    get_ldap_connection,
    host=self.login_data.get('ip', ''),
    username=self.login_data.get('username', ''),
    password=self.login_data.get('password', ''),
    domain=self.login_data.get('domain', '')
)

The LDAP module implements automatic protocol negotiation, attempting plaintext authentication on port 389 before falling back to LDAPS on port 636 if the initial connection fails.

Deep Dive into LDAP Password Handling

Doom's LDAP implementation in src/doom/protocols/ldap.py contains sophisticated logic for handling various password formats and connection scenarios.

LM Hash Detection and Formatting

The authentication system detects legacy Windows LM hashes by checking if the password consists of exactly 32 hexadecimal characters. When detected, Doom automatically prepends the standard "aad3b435b51404eeaad3b435b51404ee" string to convert the LM hash into the NTLM+LM format required by Windows authentication (ldap.py lines 12-14).

if len(password) == 32 and all(c in "0123456789abcdefABCDEF" for c in password):
    password = f"aad3b435b51404eeaad3b435b51404ee:{password}"

This conversion ensures compatibility with legacy Active Directory configurations that still accept LM hash formats.

Plaintext LDAP vs LDAPS Fallback Strategy

The get_ldap_connection function implements a resilient two-phase connection strategy. First, it attempts an NTLM bind using plaintext LDAP on port 389 (ldap.py lines 25-38). If this attempt raises an LDAPBindError or LDAPSocketOpenError, the function automatically retries using LDAPS (LDAP over TLS) on port 636 (ldap.py lines 47-60).

This fallback mechanism ensures Doom can authenticate against diverse LDAP server configurations without requiring users to manually specify encryption settings.

Error Translation and User Feedback

When authentication fails, Doom catches specific LDAP exceptions—including LDAPInvalidCredentialsResult and LDAPBindError—and translates them into user-friendly error messages. These messages are then displayed in the LoadingScreen status label (ldap.py lines 44-66 and LoadingScreen lines 100-115).

except LDAPInvalidCredentialsResult:
    self.status_label.update("Invalid credentials provided")
except LDAPBindError as e:
    self.status_label.update(f"Connection error: {str(e)}")

This error handling ensures users receive clear feedback about authentication failures rather than raw stack traces.

Implementation Examples

Basic LDAP Authentication

To manually authenticate using Doom's LDAP module:

from doom.protocols.ldap import get_ldap_connection

host = "192.168.1.10"
domain = "EXAMPLE"
username = "john.doe"
password = "Summer@2025"

conn, base_dn = get_ldap_connection(host, username, password, domain)
print(f"Connected as {conn.user} with Base DN: {base_dn}")

Handling LM Hash Authentication

When working with captured LM hashes:


# 32-character LM hash detected automatically

lm_hash = "a3b435b51404eeaad3b435b51404ee"  # Example hash

conn, base_dn = get_ldap_connection(host, username, lm_hash, domain)

# Doom automatically prepends the NTLM prefix internally

Async Integration Pattern

For non-blocking authentication in UI applications:

import asyncio
from doom.protocols.ldap import get_ldap_connection

async def authenticate_user(login_data):
    try:
        result = await asyncio.to_thread(
            get_ldap_connection,
            host=login_data["ip"],
            username=login_data["username"],
            password=login_data["password"],
            domain=login_data["domain"]
        )
        return result
    except Exception as e:
        return None

Key Source Files and Functions

Doom's password authentication implementation spans three primary modules:

  • src/doom/screens/login_screen.py – Contains the LoginScreen class responsible for UI rendering and initial password collection via query_one("#password-input").value.

  • src/doom/screens/loading_screen.py – Houses the LoadingScreen class that manages the async authentication workflow through authenticate_ldap, which calls the LDAP module in a background thread.

  • src/doom/protocols/ldap.py – Implements the core get_ldap_connection function handling LM hash detection, NTLM formatting, dual-protocol connection attempts (LDAP/LDAPS), and LDAP-specific error translation.

Summary

Doom handles password authentication through a structured three-stage pipeline that separates UI concerns from network operations:

  • Credential Collection: The LoginScreen captures masked passwords and packages them with connection metadata.
  • Async Processing: LoadingScreen offloads authentication to a background thread to prevent UI freezing.
  • LDAP Integration: The get_ldap_connection function in ldap.py automatically detects LM hashes, attempts plaintext LDAP on port 389, falls back to LDAPS on port 636, and translates low-level LDAP errors into actionable user feedback.

This architecture ensures robust authentication against enterprise Active Directory environments while maintaining responsive user interfaces.

Frequently Asked Questions

How does Doom handle password authentication?

Doom implements a three-stage authentication flow where the LoginScreen collects credentials, passes them to LoadingScreen, and executes an LDAP bind using the get_ldap_connection function. The system automatically handles both plaintext passwords and legacy LM hashes, attempting connections over plaintext LDAP (port 389) before falling back to LDAPS (port 636).

What LDAP protocols does Doom support for authentication?

According to the source code in src/doom/protocols/ldap.py, Doom supports both standard LDAP on port 389 and LDAPS (LDAP over TLS) on port 636. The get_ldap_connection function implements an automatic fallback strategy, attempting the plaintext connection first and only retrying with encryption if the initial bind fails.

How does Doom handle LM hash passwords?

When the provided password consists of exactly 32 hexadecimal characters, Doom detects it as an LM hash and automatically prepends the standard NTLM prefix aad3b435b51404eeaad3b435b51404ee before authentication. This conversion, implemented in ldap.py lines 12-14, ensures compatibility with Windows authentication protocols that expect the combined NTLM+LM format.

Where is the authentication logic implemented in Doom's codebase?

The authentication logic is distributed across three main files: src/doom/screens/login_screen.py handles UI-based credential collection, src/doom/screens/loading_screen.py manages asynchronous authentication workflows, and src/doom/protocols/ldap.py contains the core get_ldap_connection function that performs the actual LDAP bind operations and error handling.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →