mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 13:05:18 +00:00
Merge remote-tracking branch 'origin/feat/geo-board' into feat/integration-all
# Conflicts: # bitchat/Services/BLE/BLEOutboundPacketPolicy.swift # bitchat/Services/BLE/BLEService.swift # bitchat/Services/Transport.swift # bitchat/Sync/GossipSyncManager.swift # bitchat/Sync/SyncTypeFlags.swift # bitchat/ViewModels/ChatViewModel.swift # localPackages/BitFoundation/Sources/BitFoundation/MessageType.swift
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
//
|
||||
// BoardPacketsTests.swift
|
||||
// bitchatTests
|
||||
//
|
||||
// This is free and unencumbered software released into the public domain.
|
||||
// For more information, see <https://unlicense.org>
|
||||
//
|
||||
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import bitchat
|
||||
|
||||
struct BoardPacketsTests {
|
||||
|
||||
private let authorKey = Curve25519.Signing.PrivateKey()
|
||||
|
||||
private func makeSignedPost(
|
||||
geohash: String = "9q8yy",
|
||||
content: String = "water point at the north gate",
|
||||
nickname: String = "ranger",
|
||||
createdAt: UInt64 = 1_700_000_000_000,
|
||||
lifetimeMs: UInt64 = 24 * 60 * 60 * 1000,
|
||||
flags: UInt8 = 0,
|
||||
signWith key: Curve25519.Signing.PrivateKey? = nil,
|
||||
claimKey: Data? = nil
|
||||
) throws -> BoardPostPacket {
|
||||
let signer = key ?? authorKey
|
||||
let publicKey = claimKey ?? signer.publicKey.rawRepresentation
|
||||
let postID = Data((0..<16).map { _ in UInt8.random(in: 0...255) })
|
||||
let expiresAt = createdAt + lifetimeMs
|
||||
let signingBytes = BoardPostPacket.signingBytes(
|
||||
postID: postID,
|
||||
geohash: geohash,
|
||||
content: content,
|
||||
authorSigningKey: publicKey,
|
||||
authorNickname: nickname,
|
||||
createdAt: createdAt,
|
||||
expiresAt: expiresAt,
|
||||
flags: flags
|
||||
)
|
||||
let signature = try signer.signature(for: signingBytes)
|
||||
return BoardPostPacket(
|
||||
postID: postID,
|
||||
geohash: geohash,
|
||||
content: content,
|
||||
authorSigningKey: publicKey,
|
||||
authorNickname: nickname,
|
||||
createdAt: createdAt,
|
||||
expiresAt: expiresAt,
|
||||
flags: flags,
|
||||
signature: signature
|
||||
)
|
||||
}
|
||||
|
||||
private func makeSignedTombstone(
|
||||
postID: Data,
|
||||
deletedAt: UInt64 = 1_700_000_100_000,
|
||||
signWith key: Curve25519.Signing.PrivateKey? = nil,
|
||||
claimKey: Data? = nil
|
||||
) throws -> BoardTombstonePacket {
|
||||
let signer = key ?? authorKey
|
||||
let publicKey = claimKey ?? signer.publicKey.rawRepresentation
|
||||
let signature = try signer.signature(for: BoardTombstonePacket.signingBytes(postID: postID, deletedAt: deletedAt))
|
||||
return BoardTombstonePacket(
|
||||
postID: postID,
|
||||
authorSigningKey: publicKey,
|
||||
deletedAt: deletedAt,
|
||||
signature: signature
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Round trips
|
||||
|
||||
@Test func postRoundTrip() throws {
|
||||
let post = try makeSignedPost(flags: BoardPostPacket.urgentFlag)
|
||||
let encoded = BoardWire.post(post).encode()
|
||||
let decoded = try #require(BoardWire.decode(from: encoded))
|
||||
#expect(decoded == .post(post))
|
||||
#expect(decoded.verifySignature())
|
||||
guard case .post(let roundTripped) = decoded else {
|
||||
Issue.record("expected a post")
|
||||
return
|
||||
}
|
||||
#expect(roundTripped.isUrgent)
|
||||
#expect(roundTripped.geohash == "9q8yy")
|
||||
}
|
||||
|
||||
@Test func meshLocalPostRoundTrip() throws {
|
||||
let post = try makeSignedPost(geohash: "")
|
||||
let decoded = try #require(BoardWire.decode(from: BoardWire.post(post).encode()))
|
||||
#expect(decoded == .post(post))
|
||||
#expect(decoded.verifySignature())
|
||||
}
|
||||
|
||||
@Test func tombstoneRoundTrip() throws {
|
||||
let post = try makeSignedPost()
|
||||
let tombstone = try makeSignedTombstone(postID: post.postID)
|
||||
let encoded = BoardWire.tombstone(tombstone).encode()
|
||||
let decoded = try #require(BoardWire.decode(from: encoded))
|
||||
#expect(decoded == .tombstone(tombstone))
|
||||
#expect(decoded.verifySignature())
|
||||
}
|
||||
|
||||
// MARK: - Signature verification
|
||||
|
||||
@Test func forgedPostSignatureFailsVerification() throws {
|
||||
// Signed by an attacker's key but claiming the victim's key as author.
|
||||
let attacker = Curve25519.Signing.PrivateKey()
|
||||
let victim = Curve25519.Signing.PrivateKey()
|
||||
let forged = try makeSignedPost(signWith: attacker, claimKey: victim.publicKey.rawRepresentation)
|
||||
let decoded = try #require(BoardWire.decode(from: BoardWire.post(forged).encode()))
|
||||
#expect(!decoded.verifySignature())
|
||||
}
|
||||
|
||||
@Test func tamperedContentFailsVerification() throws {
|
||||
let post = try makeSignedPost(content: "meet at noon")
|
||||
let tampered = BoardPostPacket(
|
||||
postID: post.postID,
|
||||
geohash: post.geohash,
|
||||
content: "meet at midnight",
|
||||
authorSigningKey: post.authorSigningKey,
|
||||
authorNickname: post.authorNickname,
|
||||
createdAt: post.createdAt,
|
||||
expiresAt: post.expiresAt,
|
||||
flags: post.flags,
|
||||
signature: post.signature
|
||||
)
|
||||
let decoded = try #require(BoardWire.decode(from: BoardWire.post(tampered).encode()))
|
||||
#expect(!decoded.verifySignature())
|
||||
}
|
||||
|
||||
@Test func forgedTombstoneSignatureFailsVerification() throws {
|
||||
let post = try makeSignedPost()
|
||||
let attacker = Curve25519.Signing.PrivateKey()
|
||||
let forged = try makeSignedTombstone(
|
||||
postID: post.postID,
|
||||
signWith: attacker,
|
||||
claimKey: post.authorSigningKey
|
||||
)
|
||||
let decoded = try #require(BoardWire.decode(from: BoardWire.tombstone(forged).encode()))
|
||||
#expect(!decoded.verifySignature())
|
||||
}
|
||||
|
||||
// MARK: - Decode validation
|
||||
|
||||
@Test func rejectsExpiryBeyondSevenDays() throws {
|
||||
let tooLong = try makeSignedPost(lifetimeMs: BoardWireConstants.maxLifetimeMs + 1)
|
||||
#expect(BoardWire.decode(from: BoardWire.post(tooLong).encode()) == nil)
|
||||
|
||||
let exactlySevenDays = try makeSignedPost(lifetimeMs: BoardWireConstants.maxLifetimeMs)
|
||||
#expect(BoardWire.decode(from: BoardWire.post(exactlySevenDays).encode()) != nil)
|
||||
}
|
||||
|
||||
@Test func rejectsExpiryBeforeCreation() throws {
|
||||
let post = try makeSignedPost()
|
||||
let inverted = BoardPostPacket(
|
||||
postID: post.postID,
|
||||
geohash: post.geohash,
|
||||
content: post.content,
|
||||
authorSigningKey: post.authorSigningKey,
|
||||
authorNickname: post.authorNickname,
|
||||
createdAt: post.expiresAt,
|
||||
expiresAt: post.createdAt,
|
||||
flags: post.flags,
|
||||
signature: post.signature
|
||||
)
|
||||
#expect(BoardWire.decode(from: BoardWire.post(inverted).encode()) == nil)
|
||||
}
|
||||
|
||||
@Test func rejectsOversizedContent() throws {
|
||||
let oversized = try makeSignedPost(content: String(repeating: "x", count: BoardWireConstants.contentMaxBytes + 1))
|
||||
#expect(BoardWire.decode(from: BoardWire.post(oversized).encode()) == nil)
|
||||
|
||||
let maxed = try makeSignedPost(content: String(repeating: "x", count: BoardWireConstants.contentMaxBytes))
|
||||
#expect(BoardWire.decode(from: BoardWire.post(maxed).encode()) != nil)
|
||||
}
|
||||
|
||||
@Test func rejectsInvalidGeohashCharacters() throws {
|
||||
let invalid = try makeSignedPost(geohash: "9q8yA") // "A" is outside base32
|
||||
#expect(BoardWire.decode(from: BoardWire.post(invalid).encode()) == nil)
|
||||
}
|
||||
|
||||
@Test func toleratesUnknownTLVs() throws {
|
||||
let post = try makeSignedPost()
|
||||
var encoded = BoardWire.post(post).encode()
|
||||
// Append an unknown TLV; decoders must skip it.
|
||||
encoded.append(contentsOf: [0x7F, 0x00, 0x02, 0xDE, 0xAD])
|
||||
let decoded = try #require(BoardWire.decode(from: encoded))
|
||||
#expect(decoded == .post(post))
|
||||
#expect(decoded.verifySignature())
|
||||
}
|
||||
|
||||
@Test func rejectsTruncatedPayload() throws {
|
||||
let post = try makeSignedPost()
|
||||
let encoded = BoardWire.post(post).encode()
|
||||
#expect(BoardWire.decode(from: encoded.prefix(encoded.count - 1)) == nil)
|
||||
}
|
||||
|
||||
// MARK: - Urgent flag peek
|
||||
|
||||
@Test func urgentFlagPeekMatchesFullDecode() throws {
|
||||
let urgent = try makeSignedPost(flags: BoardPostPacket.urgentFlag)
|
||||
let calm = try makeSignedPost()
|
||||
let tombstone = try makeSignedTombstone(postID: calm.postID)
|
||||
#expect(BoardWire.urgentFlag(in: BoardWire.post(urgent).encode()))
|
||||
#expect(!BoardWire.urgentFlag(in: BoardWire.post(calm).encode()))
|
||||
#expect(!BoardWire.urgentFlag(in: BoardWire.tombstone(tombstone).encode()))
|
||||
#expect(!BoardWire.urgentFlag(in: Data()))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user