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

> Discover how Bitchat Android secures channels with PBKDF2 and AES-256 encryption. Learn how passwords transform into strong keys protecting your private messages.

- Repository: [permissionlesstech/bitchat-android](https://github.com/permissionlesstech/bitchat-android)
- Tags: deep-dive
- Published: 2026-08-04

---

**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](https://github.com/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`](https://github.com/permissionlesstech/bitchat-android/blob/main/CommandProcessor.kt) parses this command and forwards the password to `ChannelManager`:

```kotlin
// 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`](https://github.com/permissionlesstech/bitchat-android/blob/main/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:

```kotlin
// 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:

```kotlin
// 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:

```kotlin
// 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:

```kotlin
// 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`](https://github.com/permissionlesstech/bitchat-android/blob/main/DataManager.kt) handles this using Android SharedPreferences:

```kotlin
// 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`:

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

```

### Modal Dialog in ChatScreen

[`ChatScreen.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/ChatScreen.kt) observes this state and presents a blocking dialog:

```kotlin
// 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`:

```kotlin
// 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`](https://github.com/permissionlesstech/bitchat-android/blob/main/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`](https://github.com/permissionlesstech/bitchat-android/blob/main/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`](https://github.com/permissionlesstech/bitchat-android/blob/main/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt) | `/pass` command parsing |
| [`app/src/main/java/com/bitchat/android/ui/ChatState.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/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`](https://github.com/permissionlesstech/bitchat-android/blob/main/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`](https://github.com/permissionlesstech/bitchat-android/blob/main/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`](https://github.com/permissionlesstech/bitchat-android/blob/main/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`](https://github.com/permissionlesstech/bitchat-android/blob/main/NoiseChannelEncryption.kt)—specifically `channelPasswords` and `channelKeys`. Only the *names* of protected channels persist to disk via [`DataManager.kt`](https://github.com/permissionlesstech/bitchat-android/blob/main/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`](https://github.com/permissionlesstech/bitchat-android/blob/main/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.