Feature/signed public identity (#456)

* Require signed announces; add Ed25519 key/signature/timestamp TLVs and verify

- Protocol: AnnouncementPacket now requires ed25519PublicKey (0x03), announceSignature (0x04), and announceTimestamp (0x05). Encoder emits, decoder requires; unknown TLVs still tolerated.
- NoiseEncryptionService: add canonical announce sign/verify helpers using context 'bitchat-announce-v1'.
- BLEService: sign Announce, include TLVs; on receive verify (±5 min skew) and ignore unverified; store ed25519 key and isVerifiedNickname in PeerInfo.
- Preserve 8-byte on-wire IDs to keep BLE headers small; no per-message bloat.

* Fix Data.append usage for timestamp bytes in Packets and NoiseEncryptionService (use append(contentsOf:) on UnsafeRawBufferPointer)

* BLEService: fix non-optional announce fields and unwrap signature result; enforce required verification path

* Enforce verified-only public messages; append peerID suffix on nickname collisions in handleMessage

* Suffix duplicate nicknames with peerID prefix in snapshots and getPeerNicknames; ensures lists and messages disambiguate 'jack' vs 'jack'

* Include self nickname in collision detection: suffix remote 'jack' when it matches our nickname in lists and public chat

* UI labels: remove space before '#abcd' suffix in names (public chat, peer lists, snapshots)

* Style '#abcd' suffix light gray:
- Public chat: color suffix in sender via AttributedString segments
- People list: split name and color suffix segment; add helper splitNameSuffix()

* Debounce 'bitchatters nearby' notification: add 60s grace before reset when mesh goes empty; cancel reset when peers return

* Lighten '#abcd' suffix: use Color.secondary.opacity(0.6) in public chat sender and People list

* Toolbar peers indicator: use blue when Bluetooth peers present (was default text color)

* Toolbar peers indicator: use grey when zero peers (was red)

* Toolbar peers indicator: use system grey (Color.secondary) when zero peers, not green

* Fix crash: mutate peripherals dict on BLE queue (not main). Move scan restart to BLE queue as well.

* Reduce connect timeout churn: back off peripherals that time out (15s), increase timeout to 8s, skip non-connectable adverts, and demote timeout log to debug; clean backoff map periodically

* Mentions: support '@name#abcd' disambiguation; filter hashtag/URL styling inside mentions; deliver notifications only to the specified suffixed target when collisions exist

* Fix string interpolation in mention log (escape sequence)

* Mentions: parse '@name#abcd' in sender/receiver; render mention suffix grey (not orange/blue/underlined); ensure receiver notifications trigger for suffixed tokens

* Mentions: include own suffixed token 'name#<myIDprefix>' in valid tokens so '@name#abcd' to self is recognized and notified

* Public actions: show own system message by processing own public messages (remove early-return). Private /hug: add local system message for sender's chat.

* Grammar: change local '/hug' message to past tense ('you hugged/slapped'). Notifications: only fire 'bitchatters nearby' on rising-edge; remove 5-min re-fire and increase empty reset grace to 10m.

* Public /hug echo: add local system message so sender sees action immediately (no reliance on echo)

* Fix visibility: expose addPublicSystemMessage on ChatViewModel and use it from CommandProcessor

* use noise pubkey wip

* ttl=0 for signatures

* verification works

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
callebtc
2025-08-19 23:37:15 +02:00
committed by GitHub
co-authored by jack
parent a30b73dd99
commit 1c33a92765
7 changed files with 402 additions and 103 deletions
+18 -1
View File
@@ -192,7 +192,7 @@ struct BitchatPacket: Codable {
let recipientID: Data?
let timestamp: UInt64
let payload: Data
let signature: Data?
var signature: Data?
var ttl: UInt8
init(type: UInt8, senderID: Data, recipientID: Data?, timestamp: UInt64, payload: Data, signature: Data?, ttl: UInt8) {
@@ -236,6 +236,23 @@ struct BitchatPacket: Codable {
BinaryProtocol.encode(self)
}
/// Create binary representation for signing (without signature and TTL fields)
/// TTL is excluded because it changes during packet relay operations
func toBinaryDataForSigning() -> Data? {
// Create a copy without signature and with fixed TTL for signing
// TTL must be excluded because it changes during relay
let unsignedPacket = BitchatPacket(
type: type,
senderID: senderID,
recipientID: recipientID,
timestamp: timestamp,
payload: payload,
signature: nil, // Remove signature for signing
ttl: 0 // Use fixed TTL=0 for signing to ensure relay compatibility
)
return BinaryProtocol.encode(unsignedPacket)
}
static func from(_ data: Data) -> BitchatPacket? {
BinaryProtocol.decode(data)
}
+34 -16
View File
@@ -4,11 +4,13 @@ import Foundation
struct AnnouncementPacket {
let nickname: String
let publicKey: Data
let noisePublicKey: Data // Noise static public key (Curve25519.KeyAgreement)
let signingPublicKey: Data // Ed25519 public key for signing
private enum TLVType: UInt8 {
case nickname = 0x01
case noisePublicKey = 0x02
case signingPublicKey = 0x03
}
func encode() -> Data? {
@@ -20,11 +22,17 @@ struct AnnouncementPacket {
data.append(UInt8(nicknameData.count))
data.append(nicknameData)
// TLV for public key
guard publicKey.count <= 255 else { return nil }
// TLV for noise public key
guard noisePublicKey.count <= 255 else { return nil }
data.append(TLVType.noisePublicKey.rawValue)
data.append(UInt8(publicKey.count))
data.append(publicKey)
data.append(UInt8(noisePublicKey.count))
data.append(noisePublicKey)
// TLV for signing public key
guard signingPublicKey.count <= 255 else { return nil }
data.append(TLVType.signingPublicKey.rawValue)
data.append(UInt8(signingPublicKey.count))
data.append(signingPublicKey)
return data
}
@@ -32,12 +40,12 @@ struct AnnouncementPacket {
static func decode(from data: Data) -> AnnouncementPacket? {
var offset = 0
var nickname: String?
var publicKey: Data?
var noisePublicKey: Data?
var signingPublicKey: Data?
while offset + 2 <= data.count {
guard let type = TLVType(rawValue: data[offset]) else { return nil }
let typeRaw = data[offset]
offset += 1
let length = Int(data[offset])
offset += 1
@@ -45,16 +53,27 @@ struct AnnouncementPacket {
let value = data[offset..<offset + length]
offset += length
switch type {
case .nickname:
nickname = String(data: value, encoding: .utf8)
case .noisePublicKey:
publicKey = Data(value)
if let type = TLVType(rawValue: typeRaw) {
switch type {
case .nickname:
nickname = String(data: value, encoding: .utf8)
case .noisePublicKey:
noisePublicKey = Data(value)
case .signingPublicKey:
signingPublicKey = Data(value)
}
} else {
// Unknown TLV; skip (tolerant decoder for forward compatibility)
continue
}
}
guard let nickname = nickname, let publicKey = publicKey else { return nil }
return AnnouncementPacket(nickname: nickname, publicKey: publicKey)
guard let nickname = nickname, let noisePublicKey = noisePublicKey, let signingPublicKey = signingPublicKey else { return nil }
return AnnouncementPacket(
nickname: nickname,
noisePublicKey: noisePublicKey,
signingPublicKey: signingPublicKey
)
}
}
@@ -113,4 +132,3 @@ struct PrivateMessagePacket {
return PrivateMessagePacket(messageID: messageID, content: content)
}
}