# How Peer Identity Is Managed in Bitchat: Noise Static Keys and Persistent Identifiers

> Discover how Bitchat manages peer identity using a Noise static key and persistent identifiers for secure authentication and efficient discovery. Learn about its dual-identifier system.

- Repository: [permissionlesstech/bitchat](https://github.com/permissionlesstech/bitchat)
- Tags: internals
- Published: 2026-08-09

---

**Bitchat manages peer identity through a cryptographically bound dual-identifier system derived from a persistent Noise static key, using a full 64-character public key for authentication and a 16-character SHA-256 fingerprint for efficient routing and discovery.**

In the `permissionlesstech/bitchat` repository, peer identity management centers on the Noise static key pair generated once per device and stored in the system keychain. The protocol derives two forms of identifiers from this key: a full 32-byte public key for cryptographic handshakes and a stable 8-byte short ID embedded in every BLE packet header.

## The Dual-Identifier Architecture

Bitchat uses two distinct representations of the same underlying identity to balance security with efficiency.

### Full Noise Public Key

The **full Noise public key** is a 64-hex-character string representing 32 bytes of `Curve25519` key-agreement data. Stored in the device keychain after initial generation, this key serves as the ultimate source of truth for peer authentication. It appears directly in cryptographic handshakes and signature-binding operations, but remains hidden from passive network observers.

### Persistent Short Peer ID

The **persistent short peer ID** is a 16-hex-character (8-byte) fingerprint derived deterministically from the static public key. Implemented in [`PeerID.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PeerID.swift), this identifier is computed by taking the SHA-256 hash of the Noise public key and extracting the first 8 bytes (16 hex characters). This short ID is embedded in every BLE packet header and used for routing, neighbour discovery, and peer-list management.

## Deriving the Persistent Identifier from the Noise Static Key

When the application needs a stable identifier for display or routing, it calls the `PeerID` initializer with the raw public key data. The derivation occurs in two stages across separate source files.

First, `Data+SHA256.swift` provides the hashing implementation:

```swift
// Data+SHA256.swift (lines 13-17)
extension Data {
    func sha256Fingerprint() -> String {
        // Returns full SHA-256 hash as hex string
    }
}

```

Then, [`PeerID.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PeerID.swift) truncates this to create the short identifier:

```swift
// PeerID.swift (lines 31-34)
public init(publicKey: Data) {
    self.init(str: publicKey.sha256Fingerprint().prefix(16))
}

```

This design ensures the short ID is cryptographically bound to the static key—any change to the underlying key automatically produces a different fingerprint, breaking existing peer associations.

## Identity Verification Workflows

Bitchat verifies peer identity at two critical stages: when receiving BLE announcements and during the Noise XX handshake.

### BLE Announce Pre-flight Checks

When a device receives a BLE announce packet, [`BLEAnnounceHandlingPolicy.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEAnnounceHandlingPolicy.swift) validates that the advertised peer ID matches the hash of the announced static key. This prevents spoofing where a malicious actor claims another peer's short ID.

```swift
// BLEAnnounceHandlingPolicy.swift (lines 32-35)
let derivedPeerID = PeerID(publicKey: announcement.noisePublicKey)
guard derivedPeerID == peerID else {
    return .reject(.senderMismatch(derivedPeerID: derivedPeerID))
}

```

If the derived ID does not match the packet header, the implementation returns a `senderMismatch` rejection, dropping the connection attempt before cryptographic resources are expended.

### Noise Handshake Authentication

During the Noise XX handshake, [`NoiseSessionManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NoiseSessionManager.swift) validates the remote static key against the claimed peer ID through the `authenticatedRemoteKey` method:

```swift
// NoiseSessionManager.swift (lines 31-41)
private func authenticatedRemoteKey(_ remoteKey: Curve25519.KeyAgreement.PublicKey,
                                   matches claimedPeerID: PeerID) -> Bool {
    let rawKey = remoteKey.rawRepresentation
    if claimedPeerID.isShort {
        return PeerID(publicKey: rawKey) == claimedPeerID
    }
    if let claimedNoiseKey = claimedPeerID.noiseKey {
        return claimedNoiseKey == rawKey
    }
    return false
}

```

This function handles both short ID and full public key comparisons, ensuring the remote party possesses the private key corresponding to the claimed identity.

## Security and Persistence Guarantees

Because the short peer ID is a fingerprint rather than the key itself, passive observers cannot reconstruct the full 32-byte public key from the 16-character identifier alone. The identifier remains **persistent** across app restarts and device reboots as long as the Noise static key remains unchanged in the keychain.

This persistence provides stable routing tables without requiring a centralized identity server. However, if a device rotates its static key—documented in the repository's [`PEER-ID-ROTATION.md`](https://github.com/permissionlesstech/bitchat/blob/main/PEER-ID-ROTATION.md) design document—the derived short ID changes automatically, forcing a re-authentication with all previously known peers.

## Summary

- **Dual identifiers**: Bitchat uses a full 64-hex Noise public key for authentication and a 16-hex short ID for routing.
- **Derivation**: The short ID is computed as `SHA256(staticKey).prefix(16)` in [`PeerID.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PeerID.swift) using utilities from `Data+SHA256.swift`.
- **Verification**: [`BLEAnnounceHandlingPolicy.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEAnnounceHandlingPolicy.swift) validates BLE announces against derived IDs, while [`NoiseSessionManager.swift`](https://github.com/permissionlesstech/bitchat/blob/main/NoiseSessionManager.swift) authenticates handshake keys.
- **Binding**: The short ID is cryptographically bound to the static key; any rotation changes the identifier and breaks existing associations.
- **Storage**: The static key persists in the device keychain, providing a stable identity across sessions.

## Frequently Asked Questions

### How is the short peer ID generated from the Noise static key?

The short peer ID is generated by computing the SHA-256 hash of the 32-byte Curve25519 public key and extracting the first 8 bytes (16 hex characters). This occurs in [`PeerID.swift`](https://github.com/permissionlesstech/bitchat/blob/main/PeerID.swift) through the `sha256Fingerprint()` method defined in `Data+SHA256.swift`.

### Why does Bitchat use a shortened identifier instead of the full public key?

The short ID reduces packet overhead in BLE announcements and routing tables while preserving cryptographic binding. The full 64-character key is used only during authenticated handshakes, keeping the public key hidden from passive observers and reducing the attack surface for reconnaissance.

### What happens if a peer's Noise static key is compromised or rotated?

If the static key is rotated, the SHA-256 fingerprint changes, automatically generating a new short peer ID. According to the [`PEER-ID-ROTATION.md`](https://github.com/permissionlesstech/bitchat/blob/main/PEER-ID-ROTATION.md) design document, this breaks all existing peer associations, requiring re-authentication with the new key pair.

### How does Bitchat prevent peer ID spoofing during BLE discovery?

The [`BLEAnnounceHandlingPolicy.swift`](https://github.com/permissionlesstech/bitchat/blob/main/BLEAnnounceHandlingPolicy.swift) implementation derives the expected peer ID from the announced Noise public key and compares it to the ID claimed in the packet header. If they mismatch, the packet is rejected with a `senderMismatch` error before the handshake begins, preventing spoofing attacks.