mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-27 00:25:22 +00:00
Encrypt private media before BLE fragmentation (#1434)
Closes the last cleartext private-content path over BLE: private DM images/voice were sent as plaintext signed fileTransfer packets, TTL-relayed across the mesh, so every relay saw the full bytes. Now the complete BitchatFilePacket is encrypted as a single Noise AEAD message (inner type 0x20, matching Android) and the opaque ciphertext is fragmented. Adds an authenticated in-session capability proof (0x21 TLV: capabilities + Ed25519 key), TOFU-style downgrade pinning, a per-send consent dialog for the signed-cleartext fallback to legacy peers, and a cancellation/admission registry so cancel/delete cannot race a deferred cleartext send. Android wire constants (0x20 / 0x21 / capability bit 8) confirmed shipping. The 256-fragment preflight cap applies only to the directed fileTransfer migration fallback; encrypted media to capable peers uses the full receiver ceiling. Rebased over #1428/#1349: identity reads go through BLELocalIdentityStateStore; the session-bound authenticated signing-key check and the announce-path TOFU pin are kept as complementary checks. Full local suite green (1744+197 tests).
This commit is contained in:
@@ -493,6 +493,59 @@ final class SecureIdentityStateManagerTests: XCTestCase {
|
||||
XCTAssertTrue(cleared)
|
||||
}
|
||||
|
||||
func test_privateMediaCapabilityPinPersistsMonotonicallyAndPanicClearRemovesIt() async {
|
||||
let keychain = MockKeychain()
|
||||
let fingerprint = Data(repeating: 0x42, count: 32).sha256Fingerprint()
|
||||
let manager = SecureIdentityStateManager(keychain)
|
||||
|
||||
XCTAssertFalse(manager.hasObservedPrivateMediaCapability(fingerprint: fingerprint))
|
||||
manager.markPrivateMediaCapable(fingerprint: fingerprint)
|
||||
XCTAssertTrue(
|
||||
manager.hasObservedPrivateMediaCapability(fingerprint: fingerprint),
|
||||
"pin insertion must be synchronously visible to the next downgrade decision"
|
||||
)
|
||||
|
||||
// Re-marking is idempotent, and the encrypted cache carries the pin
|
||||
// across launches.
|
||||
manager.markPrivateMediaCapable(fingerprint: fingerprint)
|
||||
manager.forceSave()
|
||||
let reloaded = SecureIdentityStateManager(keychain)
|
||||
XCTAssertTrue(reloaded.hasObservedPrivateMediaCapability(fingerprint: fingerprint))
|
||||
|
||||
// ChatViewModel's panic path calls this same wipe after deleting
|
||||
// keychain data; the in-memory pin must disappear immediately too.
|
||||
reloaded.clearAllIdentityData()
|
||||
let cleared = await waitUntil {
|
||||
!reloaded.hasObservedPrivateMediaCapability(fingerprint: fingerprint)
|
||||
}
|
||||
XCTAssertTrue(cleared)
|
||||
}
|
||||
|
||||
func test_noiseAuthenticatedSigningKeyBindingPersistsAndPanicClearRemovesIt() async {
|
||||
let keychain = MockKeychain()
|
||||
let fingerprint = Data(repeating: 0x31, count: 32).sha256Fingerprint()
|
||||
let firstKey = Data(repeating: 0x41, count: 32)
|
||||
let rotatedKey = Data(repeating: 0x42, count: 32)
|
||||
let manager = SecureIdentityStateManager(keychain)
|
||||
|
||||
manager.bindAuthenticatedSigningPublicKey(firstKey, fingerprint: fingerprint)
|
||||
XCTAssertEqual(manager.authenticatedSigningPublicKey(forFingerprint: fingerprint), firstKey)
|
||||
// A later authenticated Noise session may legitimately rotate the
|
||||
// announcement signing key.
|
||||
manager.bindAuthenticatedSigningPublicKey(rotatedKey, fingerprint: fingerprint)
|
||||
XCTAssertEqual(manager.authenticatedSigningPublicKey(forFingerprint: fingerprint), rotatedKey)
|
||||
|
||||
manager.forceSave()
|
||||
let reloaded = SecureIdentityStateManager(keychain)
|
||||
XCTAssertEqual(reloaded.authenticatedSigningPublicKey(forFingerprint: fingerprint), rotatedKey)
|
||||
|
||||
reloaded.clearAllIdentityData()
|
||||
let cleared = await waitUntil {
|
||||
reloaded.authenticatedSigningPublicKey(forFingerprint: fingerprint) == nil
|
||||
}
|
||||
XCTAssertTrue(cleared)
|
||||
}
|
||||
|
||||
func test_forceSave_withFailingCacheWriteDoesNotPersistCache() async {
|
||||
let keychain = FailingCacheSaveKeychain()
|
||||
let manager = SecureIdentityStateManager(keychain)
|
||||
|
||||
Reference in New Issue
Block a user