How Bitchat Android Secures Channels with Passwords: PBKDF2 and AES-256 Encryption Explained

Bitchat Android protects group channels by converting user-supplied passwords into AES-256 keys using PBKDF2 key derivation, then encrypting all messages with these channel-specific keys stored in the NoiseChannelEncryption component.

The permissionlesstech/bitchat-android repository implements password-protected channels through a multi-layered cryptographic system. Channel security combines in-memory key storage, deterministic key derivation, and reactive UI prompts to ensure only authorized users can read encrypted messages.


How Channel Password Protection Works

Password protection in Bitchat Android follows a clear operational flow from user command to message encryption. Understanding each stage helps developers audit or extend the security model.

User Initiates Password Protection with /pass

When a user types /pass <password> in the chat interface, CommandProcessor.kt parses this command and forwards the password to ChannelManager:

// CommandProcessor.kt (lines 64-66)
val parts = text.split(' ')
val password = if (parts.size > 2) parts[2] else null
channelManager.setChannelPassword(currentChannel, password!!)

The CommandProcessor acts as the entry point for all slash commands, delegating password handling to the channel management layer.


PBKDF2 Key Derivation in NoiseChannelEncryption

The core security mechanism resides in NoiseChannelEncryption.kt, where passwords transform into cryptographic keys through PBKDF2WithHmacSHA256.

Deriving AES-256 Keys from Passwords

setChannelPassword stores the raw password and derives the encryption key in one operation:

// NoiseChannelEncryption.kt (lines 39-49)
fun setChannelPassword(channel: String, password: String) {
    channelPasswords[channel] = password
    channelKeys[channel] = deriveChannelKey(password, channel)
    // ... logging
}

The deriveChannelKey function implements industry-standard key stretching:

// NoiseChannelEncryption.kt (lines 146-156)
private fun deriveChannelKey(password: String, channel: String): SecretKeySpec {
    val spec = PBEKeySpec(
        password.toCharArray(),
        (channel + "salt").toByteArray(),  // channel-specific salt
        65536,                              // iterations
        256                                 // bits
    )
    val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
    val secret = factory.generateSecret(spec)
    return SecretKeySpec(secret.encoded, "AES")
}

Security parameters:

  • Algorithm: PBKDF2 with HMAC-SHA256
  • Iterations: 65,536 (resistant to brute-force attacks)
  • Key length: 256 bits (AES-256 compatible)
  • Salt derivation: Channel name plus static string "salt"

In-Memory Key Storage and Password Verification

Bitchat Android maintains two concurrent maps for fast cryptographic operations:

// Inside NoiseChannelEncryption class
private val channelPasswords = ConcurrentHashMap<String, String>()  // raw passwords
private val channelKeys = ConcurrentHashMap<String, SecretKeySpec>() // derived keys

Verifying Channel Passwords on Join

When joining a protected channel, ChannelManager.verifyChannelPassword validates the supplied password without network round-trips:

// ChannelManager.kt (lines 30-38)
fun verifyChannelPassword(channel: String, password: String): Boolean {
    val storedKey = channelKeys[channel]
    if (storedKey == null) return false
    
    val testKey = deriveChannelKey(password, channel)
    return storedKey == testKey
}

This design enables offline password verification—the derived key comparison happens entirely client-side.


Persistent Storage of Protected Channel Names

While keys remain in memory, the fact that a channel requires a password persists across app restarts. DataManager.kt handles this using Android SharedPreferences:

// DataManager.kt (lines 83-108)
fun savePasswordProtectedChannels(channels: Set<String>) {
    prefs.edit().putStringSet("password_protected_channels", channels).apply()
}

fun getPasswordProtectedChannels(): Set<String> {
    return prefs.getStringSet("password_protected_channels", emptySet()) ?: emptySet()
}

Critical security note: Only channel names are persisted. The password and derived key reconstruct from user input on each fresh app launch. This minimizes exposure of cryptographic material.


Reactive UI for Password Prompts

The password flow integrates with Jetpack Compose through ChatState and ChatScreen.

State Management in ChatState

When the protocol signals a password requirement, ChatState exposes this through a StateFlow:

// ChatState.kt (lines 77-78)
private val _passwordPromptChannel = MutableStateFlow<String?>(null)
val passwordPromptChannel: StateFlow<String?> = _passwordPromptChannel.asStateFlow()

ChatScreen.kt observes this state and presents a blocking dialog:

// ChatScreen.kt (lines 110-124)
val passwordPromptChannel by viewModel.passwordPromptChannel.collectAsStateWithLifecycle()

if (passwordPromptChannel != null) {
    PasswordDialog(
        channel = passwordPromptChannel!!,
        onConfirm = { pw -> 
            viewModel.joinChannel(passwordPromptChannel!!, pw) 
        },
        onDismiss = { viewModel.clearPasswordPrompt() }
    )
}

This reactive pattern ensures password prompts appear immediately when needed, without polling or callback complexity.


Message Encryption and Decryption Flow

Once authenticated, all channel traffic uses the stored SecretKeySpec:

  • Encryption: NoiseChannelEncryption.encryptMessageForChannel encrypts outbound messages with the channel-specific AES key
  • Decryption: decryptMessageForChannel validates and decrypts inbound packets using the same key

Both operations reference channelKeys[channel] for key material, falling back to unencrypted transport if no key exists.


Modifying or Removing Channel Passwords

The /pass command supports three operations via ChannelManager.setChannelPassword:

  1. Set new password — overwrites existing key derivation
  2. Change password — same as set, re-derives key with new input
  3. Remove protection — handled by removeChannelPassword:
// NoiseChannelEncryption.kt (lines 60-65)
fun removeChannelPassword(channel: String) {
    channelPasswords.remove(channel)
    channelKeys.remove(channel)
    // ... logging and state cleanup
}

Removing a password clears both maps and updates the persisted channel set in DataManager.


Key Implementation Files

File Responsibility
app/src/main/java/com/bitchat/android/noise/NoiseChannelEncryption.kt PBKDF2 derivation, key storage, encrypt/decrypt operations
app/src/main/java/com/bitchat/android/ui/ChannelManager.kt Channel joining, password verification, coordination
app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt /pass command parsing
app/src/main/java/com/bitchat/android/ui/ChatState.kt Reactive state for password prompts
app/src/main/java/com/bitchat/android/ui/ChatScreen.kt Compose UI for password dialog
app/src/main/java/com/bitchat/android/ui/DataManager.kt Persistent storage of protected channel names

Summary

  • Password-to-key conversion uses PBKDF2-HMAC-SHA256 with 65,536 iterations and channel-derived salts
  • Dual map architecture separates raw passwords (channelPasswords) from derived keys (channelKeys) in NoiseChannelEncryption.kt
  • Offline verification enables password checking without server coordination via ChannelManager.verifyChannelPassword
  • Minimal persistence stores only channel names, never keys or passwords, in Android SharedPreferences
  • Reactive UI layer leverages StateFlow in ChatState for immediate password prompt display

Frequently Asked Questions

What encryption algorithm does Bitchat Android use for channel passwords?

Bitchat Android uses AES-256 for message encryption, with keys derived via PBKDF2WithHmacSHA256. The key derivation runs 65,536 iterations with a channel-specific salt, producing 256-bit keys stored as SecretKeySpec objects.

Where are channel passwords stored in Bitchat Android?

Passwords and derived keys reside in in-memory ConcurrentHashMap instances inside NoiseChannelEncryption.kt—specifically channelPasswords and channelKeys. Only the names of protected channels persist to disk via DataManager.kt; actual cryptographic material never touches persistent storage.

How does Bitchat Android prompt users for channel passwords?

The ChatState class exposes a passwordPromptChannel StateFlow that ChatScreen.kt observes. When a protected channel requires authentication, this state becomes non-null, triggering a modal PasswordDialog composable where users enter credentials for ChannelManager.joinChannel to verify.

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 →