Fix crash: Add thread safety to EncryptionService

The crash was caused by concurrent access to CryptoKit signing operations
from multiple threads. Added thread-safe access using a concurrent queue
with barriers for write operations and sync reads.

This fixes the EXC_BAD_ACCESS crash seen in TestFlight build.
This commit is contained in:
jack
2025-07-06 21:02:47 +02:00
parent 7c7e23b154
commit 10656b49eb
2 changed files with 72 additions and 46 deletions
+12 -4
View File
@@ -482,6 +482,7 @@
DEVELOPMENT_TEAM = L3N5LHJD5Y; DEVELOPMENT_TEAM = L3N5LHJD5Y;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = bitchat/Info.plist; INFOPLIST_FILE = bitchat/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -491,9 +492,12 @@
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
PRODUCT_NAME = bitchat; PRODUCT_NAME = bitchat;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = 1;
}; };
name = Debug; name = Debug;
}; };
@@ -528,6 +532,7 @@
DEVELOPMENT_TEAM = L3N5LHJD5Y; DEVELOPMENT_TEAM = L3N5LHJD5Y;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = bitchat/Info.plist; INFOPLIST_FILE = bitchat/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 16.0; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -537,9 +542,12 @@
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat; PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
PRODUCT_NAME = bitchat; PRODUCT_NAME = bitchat;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = 1;
}; };
name = Release; name = Release;
}; };
+60 -42
View File
@@ -28,6 +28,9 @@ class EncryptionService {
private let identityKey: Curve25519.Signing.PrivateKey private let identityKey: Curve25519.Signing.PrivateKey
public let identityPublicKey: Curve25519.Signing.PublicKey public let identityPublicKey: Curve25519.Signing.PublicKey
// Thread safety
private let cryptoQueue = DispatchQueue(label: "chat.bitchat.crypto", attributes: .concurrent)
init() { init() {
// Generate ephemeral key pairs for this session // Generate ephemeral key pairs for this session
self.privateKey = Curve25519.KeyAgreement.PrivateKey() self.privateKey = Curve25519.KeyAgreement.PrivateKey()
@@ -59,46 +62,50 @@ class EncryptionService {
// Add peer's combined public keys // Add peer's combined public keys
func addPeerPublicKey(_ peerID: String, publicKeyData: Data) throws { func addPeerPublicKey(_ peerID: String, publicKeyData: Data) throws {
// Convert to array for safe access try cryptoQueue.sync(flags: .barrier) {
let keyBytes = [UInt8](publicKeyData) // Convert to array for safe access
let keyBytes = [UInt8](publicKeyData)
guard keyBytes.count == 96 else {
// print("[CRYPTO] Invalid public key data size: \(keyBytes.count), expected 96") guard keyBytes.count == 96 else {
throw EncryptionError.invalidPublicKey // print("[CRYPTO] Invalid public key data size: \(keyBytes.count), expected 96")
} throw EncryptionError.invalidPublicKey
}
// Extract all three keys: 32 for key agreement + 32 for signing + 32 for identity
let keyAgreementData = Data(keyBytes[0..<32]) // Extract all three keys: 32 for key agreement + 32 for signing + 32 for identity
let signingKeyData = Data(keyBytes[32..<64]) let keyAgreementData = Data(keyBytes[0..<32])
let identityKeyData = Data(keyBytes[64..<96]) let signingKeyData = Data(keyBytes[32..<64])
let identityKeyData = Data(keyBytes[64..<96])
let publicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: keyAgreementData)
peerPublicKeys[peerID] = publicKey let publicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: keyAgreementData)
peerPublicKeys[peerID] = publicKey
let signingKey = try Curve25519.Signing.PublicKey(rawRepresentation: signingKeyData)
peerSigningKeys[peerID] = signingKey let signingKey = try Curve25519.Signing.PublicKey(rawRepresentation: signingKeyData)
peerSigningKeys[peerID] = signingKey
let identityKey = try Curve25519.Signing.PublicKey(rawRepresentation: identityKeyData)
peerIdentityKeys[peerID] = identityKey let identityKey = try Curve25519.Signing.PublicKey(rawRepresentation: identityKeyData)
peerIdentityKeys[peerID] = identityKey
// Stored all three keys for peer
// Stored all three keys for peer
// Generate shared secret for encryption
if let publicKey = peerPublicKeys[peerID] { // Generate shared secret for encryption
let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: publicKey) if let publicKey = peerPublicKeys[peerID] {
let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey( let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: publicKey)
using: SHA256.self, let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(
salt: "bitchat-v1".data(using: .utf8)!, using: SHA256.self,
sharedInfo: Data(), salt: "bitchat-v1".data(using: .utf8)!,
outputByteCount: 32 sharedInfo: Data(),
) outputByteCount: 32
sharedSecrets[peerID] = symmetricKey )
sharedSecrets[peerID] = symmetricKey
}
} }
} }
// Get peer's persistent identity key for favorites // Get peer's persistent identity key for favorites
func getPeerIdentityKey(_ peerID: String) -> Data? { func getPeerIdentityKey(_ peerID: String) -> Data? {
return peerIdentityKeys[peerID]?.rawRepresentation return cryptoQueue.sync {
return peerIdentityKeys[peerID]?.rawRepresentation
}
} }
// Clear persistent identity (for panic mode) // Clear persistent identity (for panic mode)
@@ -108,8 +115,11 @@ class EncryptionService {
} }
func encrypt(_ data: Data, for peerID: String) throws -> Data { func encrypt(_ data: Data, for peerID: String) throws -> Data {
guard let symmetricKey = sharedSecrets[peerID] else { let symmetricKey = try cryptoQueue.sync {
throw EncryptionError.noSharedSecret guard let key = sharedSecrets[peerID] else {
throw EncryptionError.noSharedSecret
}
return key
} }
let sealedBox = try AES.GCM.seal(data, using: symmetricKey) let sealedBox = try AES.GCM.seal(data, using: symmetricKey)
@@ -117,8 +127,11 @@ class EncryptionService {
} }
func decrypt(_ data: Data, from peerID: String) throws -> Data { func decrypt(_ data: Data, from peerID: String) throws -> Data {
guard let symmetricKey = sharedSecrets[peerID] else { let symmetricKey = try cryptoQueue.sync {
throw EncryptionError.noSharedSecret guard let key = sharedSecrets[peerID] else {
throw EncryptionError.noSharedSecret
}
return key
} }
let sealedBox = try AES.GCM.SealedBox(combined: data) let sealedBox = try AES.GCM.SealedBox(combined: data)
@@ -126,12 +139,17 @@ class EncryptionService {
} }
func sign(_ data: Data) throws -> Data { func sign(_ data: Data) throws -> Data {
return try signingPrivateKey.signature(for: data) // Create a local copy of the key to avoid concurrent access
let key = signingPrivateKey
return try key.signature(for: data)
} }
func verify(_ signature: Data, for data: Data, from peerID: String) throws -> Bool { func verify(_ signature: Data, for data: Data, from peerID: String) throws -> Bool {
guard let verifyingKey = peerSigningKeys[peerID] else { let verifyingKey = try cryptoQueue.sync {
throw EncryptionError.noSharedSecret guard let key = peerSigningKeys[peerID] else {
throw EncryptionError.noSharedSecret
}
return key
} }
return verifyingKey.isValidSignature(signature, for: data) return verifyingKey.isValidSignature(signature, for: data)