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()
Modal Dialog in ChatScreen
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.encryptMessageForChannelencrypts outbound messages with the channel-specific AES key - Decryption:
decryptMessageForChannelvalidates 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:
- Set new password — overwrites existing key derivation
- Change password — same as set, re-derives key with new input
- 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) inNoiseChannelEncryption.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
StateFlowinChatStatefor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →