Initial commit: Bluetooth mesh chat app with end-to-end encryption

This commit is contained in:
jack
2025-07-02 19:18:35 +02:00
commit 7e90bf6369
41 changed files with 2000 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## User settings
xcuserdata/
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
## Gcc Patch
/*.gcno
## macOS
.DS_Store
## SPM
.swiftpm
.build/
## CocoaPods
Pods/
## Carthage
Carthage/Build/
## fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
## Code Injection
iOSInjectionProject/
## Xcode project
*.xcodeproj/project.xcworkspace/
*.xcodeproj/xcshareddata/
## Python
__pycache__/
*.py[cod]
*$py.class
## Temporary files
*.tmp
*.temp
+26
View File
@@ -0,0 +1,26 @@
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "bitchat",
platforms: [
.iOS(.v16),
.macOS(.v13)
],
products: [
.executable(
name: "bitchat",
targets: ["bitchat"]
),
],
targets: [
.executableTarget(
name: "bitchat",
path: "bitchat",
resources: [
.process("Info.plist")
]
),
]
)
+85
View File
@@ -0,0 +1,85 @@
# bitchat
A secure, end-to-end encrypted Bluetooth mesh chat application with an IRC-style interface.
## Features
- End-to-end encryption using Curve25519 and AES-GCM
- Bluetooth mesh networking with automatic peer discovery
- Message relay capability (TTL-based flooding)
- IRC-style terminal interface
- Persistent nickname storage
- Universal app (iOS and macOS)
- No internet connection required
## Setup
### Option 1: Using XcodeGen (Recommended)
1. Install XcodeGen if you haven't already:
```bash
brew install xcodegen
```
2. Generate the Xcode project:
```bash
cd bitchat
xcodegen generate
```
3. Open the generated project:
```bash
open bitchat.xcodeproj
```
### Option 2: Using Swift Package Manager
1. Open the project in Xcode:
```bash
cd bitchat
open Package.swift
```
2. Select your target device and run
### Option 3: Manual Xcode Project
1. Open Xcode and create a new iOS/macOS App
2. Copy all Swift files from the `bitchat` directory into your project
3. Update Info.plist with Bluetooth permissions
4. Set deployment target to iOS 16.0 / macOS 13.0
## Usage
1. Launch the app on multiple devices
2. Choose or modify your nickname
3. The app will automatically discover nearby peers
4. Start chatting! Messages are relayed through the mesh network
## Security
- All messages are end-to-end encrypted
- Public key exchange happens automatically on connection
- Messages are signed to prevent tampering
- TTL prevents infinite message loops
## Protocol
The bitchat protocol uses JSON-encoded packets with the following structure:
- Packet versioning for future compatibility
- Message types: handshake, message, ack, relay, announce, keyExchange
- TTL-based flooding for mesh relay
- Signature verification for authenticity
## Building for Production
1. Set your development team in project settings
2. Configure code signing
3. Archive and distribute through App Store or TestFlight
## Android Compatibility
The protocol is designed to be platform-agnostic. An Android client can be built using:
- Bluetooth LE APIs
- Same packet structure and encryption
- Compatible service/characteristic UUIDs
+483
View File
@@ -0,0 +1,483 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 63;
objects = {
/* Begin PBXBuildFile section */
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; };
1D9674FA5F998503831DC281 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; };
6DE056E1EE9850E9FBF50157 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; };
6E7761E21C99F28AE2F9BE5F /* BitchatApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF625BB3AD919322C01A46B2 /* BitchatApp.swift */; };
739429DFDE5C5829CF70DA7D /* EncryptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC1563390A15C042D059CF9 /* EncryptionService.swift */; };
7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; };
7A50E2F04A3515A7E90EEAE4 /* BluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */; };
7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; };
923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; };
92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A08E03AA0C63E97C91749AEC /* ContentView.swift */; };
BCCFEDC1EBE59323C3C470BF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; };
D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; };
D948085736ED8E736C1DE3B0 /* BluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */; };
DDA1DFAB1FF7AADE52DC0F53 /* EncryptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC1563390A15C042D059CF9 /* EncryptionService.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatProtocol.swift; sourceTree = "<group>"; };
3A69677D382F1C3D5ED03F7D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
6DC1563390A15C042D059CF9 /* EncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionService.swift; sourceTree = "<group>"; };
7EEBDA723E1CFD88758DA4AC /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
997D512074C64904D75DDD40 /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMeshService.swift; sourceTree = "<group>"; };
E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewModel.swift; sourceTree = "<group>"; };
EA706D8E5097785414646A8E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
EF625BB3AD919322C01A46B2 /* BitchatApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatApp.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXGroup section */
18198ED912AAF495D8AF7763 = {
isa = PBXGroup;
children = (
2F82C5FC8433F4064F079D1F /* bitchat */,
9F37F9F2C353B58AC809E93B /* Products */,
);
sourceTree = "<group>";
};
2F82C5FC8433F4064F079D1F /* bitchat */ = {
isa = PBXGroup;
children = (
3A69677D382F1C3D5ED03F7D /* Assets.xcassets */,
EF625BB3AD919322C01A46B2 /* BitchatApp.swift */,
EA706D8E5097785414646A8E /* Info.plist */,
ADD53BCDA233C02E53458926 /* Protocols */,
D98A3186D7E4C72E35BDF7FE /* Services */,
45BB7D87CAE42A8C0447D909 /* ViewModels */,
A55126E93155456CAA8D6656 /* Views */,
);
path = bitchat;
sourceTree = "<group>";
};
45BB7D87CAE42A8C0447D909 /* ViewModels */ = {
isa = PBXGroup;
children = (
E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
};
9F37F9F2C353B58AC809E93B /* Products */ = {
isa = PBXGroup;
children = (
997D512074C64904D75DDD40 /* bitchat.app */,
7EEBDA723E1CFD88758DA4AC /* bitchat.app */,
);
name = Products;
sourceTree = "<group>";
};
A55126E93155456CAA8D6656 /* Views */ = {
isa = PBXGroup;
children = (
A08E03AA0C63E97C91749AEC /* ContentView.swift */,
);
path = Views;
sourceTree = "<group>";
};
ADD53BCDA233C02E53458926 /* Protocols */ = {
isa = PBXGroup;
children = (
229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */,
);
path = Protocols;
sourceTree = "<group>";
};
D98A3186D7E4C72E35BDF7FE /* Services */ = {
isa = PBXGroup;
children = (
D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */,
6DC1563390A15C042D059CF9 /* EncryptionService.swift */,
);
path = Services;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
0576A29205865664C0937536 /* bitchat_macOS */ = {
isa = PBXNativeTarget;
buildConfigurationList = DA5644925338B8189B035657 /* Build configuration list for PBXNativeTarget "bitchat_macOS" */;
buildPhases = (
137ABE739BF20ACDDF8CC605 /* Sources */,
0214973A876129753D39EB47 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = bitchat_macOS;
packageProductDependencies = (
);
productName = bitchat_macOS;
productReference = 7EEBDA723E1CFD88758DA4AC /* bitchat.app */;
productType = "com.apple.product-type.application";
};
AF077EA0474EDEDE2C72716C /* bitchat_iOS */ = {
isa = PBXNativeTarget;
buildConfigurationList = 53EADEF7546F94DDF82271B9 /* Build configuration list for PBXNativeTarget "bitchat_iOS" */;
buildPhases = (
4E49E34F00154C051AE90FED /* Sources */,
CD6E8F32BC38357473954F97 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = bitchat_iOS;
packageProductDependencies = (
);
productName = bitchat_iOS;
productReference = 997D512074C64904D75DDD40 /* bitchat.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
475D96681D0EA0AE57A4E06E /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1430;
TargetAttributes = {
0576A29205865664C0937536 = {
DevelopmentTeam = "";
ProvisioningStyle = Automatic;
};
AF077EA0474EDEDE2C72716C = {
ProvisioningStyle = Automatic;
};
};
};
buildConfigurationList = 3EA424CBD51200895D361189 /* Build configuration list for PBXProject "bitchat" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
Base,
en,
);
mainGroup = 18198ED912AAF495D8AF7763;
minimizedProjectReferenceProxies = 1;
projectDirPath = "";
projectRoot = "";
targets = (
AF077EA0474EDEDE2C72716C /* bitchat_iOS */,
0576A29205865664C0937536 /* bitchat_macOS */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
0214973A876129753D39EB47 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
CD6E8F32BC38357473954F97 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
BCCFEDC1EBE59323C3C470BF /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
137ABE739BF20ACDDF8CC605 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
6E7761E21C99F28AE2F9BE5F /* BitchatApp.swift in Sources */,
923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */,
7A50E2F04A3515A7E90EEAE4 /* BluetoothMeshService.swift in Sources */,
D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */,
92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */,
739429DFDE5C5829CF70DA7D /* EncryptionService.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
4E49E34F00154C051AE90FED /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
10E68BB889356219189E38EC /* BitchatApp.swift in Sources */,
6DE056E1EE9850E9FBF50157 /* BitchatProtocol.swift in Sources */,
D948085736ED8E736C1DE3B0 /* BluetoothMeshService.swift in Sources */,
7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */,
1D9674FA5F998503831DC281 /* ContentView.swift in Sources */,
DDA1DFAB1FF7AADE52DC0F53 /* EncryptionService.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
702E7395723CADA4B830F4A9 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = L3N5LHJD5Y;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = bitchat/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
PRODUCT_BUNDLE_IDENTIFIER = net.bitchat.app;
PRODUCT_NAME = bitchat;
SDKROOT = iphoneos;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
B36671AEACCBF92BE10852E9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = L3N5LHJD5Y;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = bitchat/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
PRODUCT_BUNDLE_IDENTIFIER = net.bitchat.app;
PRODUCT_NAME = bitchat;
SDKROOT = iphoneos;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
BB044400A0F06B93F22D0D55 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = bitchat/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
PRODUCT_BUNDLE_IDENTIFIER = com.bitchat.app;
PRODUCT_NAME = bitchat;
SDKROOT = macosx;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
BF0D85727BCB6E346962F419 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 5.0;
};
name = Release;
};
CC79F65842D42034ACEE79B7 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = bitchat/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
PRODUCT_BUNDLE_IDENTIFIER = com.bitchat.app;
PRODUCT_NAME = bitchat;
SDKROOT = macosx;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
D8C5BF109BB2630752185FA0 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = "";
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"DEBUG=1",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
3EA424CBD51200895D361189 /* Build configuration list for PBXProject "bitchat" */ = {
isa = XCConfigurationList;
buildConfigurations = (
D8C5BF109BB2630752185FA0 /* Debug */,
BF0D85727BCB6E346962F419 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
53EADEF7546F94DDF82271B9 /* Build configuration list for PBXNativeTarget "bitchat_iOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
702E7395723CADA4B830F4A9 /* Debug */,
B36671AEACCBF92BE10852E9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
DA5644925338B8189B035657 /* Build configuration list for PBXNativeTarget "bitchat_macOS" */ = {
isa = XCConfigurationList;
buildConfigurations = (
CC79F65842D42034ACEE79B7 /* Debug */,
BB044400A0F06B93F22D0D55 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
/* End XCConfigurationList section */
};
rootObject = 475D96681D0EA0AE57A4E06E /* Project object */;
}
@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "1.000",
"red" : "0.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,176 @@
{
"images" : [
{
"filename" : "icon_20x20@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "icon_20x20@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"filename" : "icon_29x29@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "icon_29x29@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"filename" : "icon_40x40@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "icon_40x40@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"filename" : "icon_60x60@2x.png",
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"filename" : "icon_60x60@3x.png",
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"filename" : "icon_20x20.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"filename" : "icon_20x20@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"filename" : "icon_29x29.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"filename" : "icon_29x29@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"filename" : "icon_40x40.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"filename" : "icon_40x40@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"filename" : "icon_76x76.png",
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"filename" : "icon_76x76@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"filename" : "icon_83.5x83.5@2x.png",
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"filename" : "icon_1024x1024.png",
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
},
{
"filename" : "icon_16x16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "icon_16x16@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "icon_32x32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "icon_32x32@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "icon_128x128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "icon_128x128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "icon_256x256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "icon_256x256@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "icon_512x512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "icon_512x512@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 848 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 B

+6
View File
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
+17
View File
@@ -0,0 +1,17 @@
import SwiftUI
@main
struct BitchatApp: App {
@StateObject private var chatViewModel = ChatViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(chatViewModel)
}
#if os(macOS)
.windowStyle(.hiddenTitleBar)
.windowResizability(.contentSize)
#endif
}
}
+42
View File
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>bitchat</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>bitchat uses Bluetooth to discover and connect with other bitchat users nearby.</string>
<key>UILaunchScreen</key>
<dict>
<key>UIColorName</key>
<string>Black</string>
</dict>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
+66
View File
@@ -0,0 +1,66 @@
import Foundation
import CryptoKit
enum MessageType: UInt8 {
case handshake = 0x01
case message = 0x02
case ack = 0x03
case relay = 0x04
case announce = 0x05
case keyExchange = 0x06
}
struct BitchatPacket: Codable {
let version: UInt8
let type: UInt8
let senderID: Data
let recipientID: Data?
let timestamp: UInt64
let payload: Data
let signature: Data?
var ttl: UInt8
init(type: UInt8, senderID: Data, recipientID: Data?, timestamp: UInt64, payload: Data, signature: Data?, ttl: UInt8) {
self.version = 1
self.type = type
self.senderID = senderID
self.recipientID = recipientID
self.timestamp = timestamp
self.payload = payload
self.signature = signature
self.ttl = ttl
}
var data: Data? {
try? JSONEncoder().encode(self)
}
static func from(_ data: Data) -> BitchatPacket? {
try? JSONDecoder().decode(BitchatPacket.self, from: data)
}
}
struct BitchatMessage: Codable {
let id: String
let sender: String
let content: String
let timestamp: Date
let isRelay: Bool
let originalSender: String?
init(sender: String, content: String, timestamp: Date, isRelay: Bool, originalSender: String? = nil) {
self.id = UUID().uuidString
self.sender = sender
self.content = content
self.timestamp = timestamp
self.isRelay = isRelay
self.originalSender = originalSender
}
}
protocol BitchatDelegate: AnyObject {
func didReceiveMessage(_ message: BitchatMessage)
func didConnectToPeer(_ peerID: String)
func didDisconnectFromPeer(_ peerID: String)
func didUpdatePeerList(_ peers: [String])
}
+528
View File
@@ -0,0 +1,528 @@
import Foundation
import CoreBluetooth
import Combine
class BluetoothMeshService: NSObject {
static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C")
static let characteristicUUID = CBUUID(string: "A1B2C3D4-E5F6-4A5B-8C9D-0E1F2A3B4C5D")
private var centralManager: CBCentralManager!
private var peripheralManager: CBPeripheralManager!
private var discoveredPeripherals: [CBPeripheral] = []
private var connectedPeripherals: [String: CBPeripheral] = [:]
private var peripheralCharacteristics: [CBPeripheral: CBCharacteristic] = [:]
private var characteristic: CBMutableCharacteristic!
private var subscribedCentrals: [CBCentral] = []
private var peerNicknames: [String: String] = [:]
private var activePeers: Set<String> = [] // Track all active peers
weak var delegate: BitchatDelegate?
private let encryptionService = EncryptionService()
private let messageQueue = DispatchQueue(label: "bitchat.messageQueue", attributes: .concurrent)
private var processedMessages = Set<String>()
private let maxTTL: UInt8 = 5
let myPeerID: String
override init() {
self.myPeerID = UUID().uuidString.prefix(8).lowercased()
super.init()
centralManager = CBCentralManager(delegate: self, queue: nil)
peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
}
func startServices() {
// Start both central and peripheral services
if centralManager.state == .poweredOn {
startScanning()
}
if peripheralManager.state == .poweredOn {
setupPeripheral()
startAdvertising()
}
}
func startAdvertising() {
guard peripheralManager.state == .poweredOn else {
return
}
let advertisementData: [String: Any] = [
CBAdvertisementDataServiceUUIDsKey: [BluetoothMeshService.serviceUUID],
CBAdvertisementDataLocalNameKey: "bitchat-\(myPeerID)"
]
peripheralManager.startAdvertising(advertisementData)
}
func startScanning() {
guard centralManager.state == .poweredOn else {
return
}
centralManager.scanForPeripherals(withServices: [BluetoothMeshService.serviceUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false])
}
private func setupPeripheral() {
let characteristic = CBMutableCharacteristic(
type: BluetoothMeshService.characteristicUUID,
properties: [.read, .write, .writeWithoutResponse, .notify],
value: nil,
permissions: [.readable, .writeable]
)
let service = CBMutableService(type: BluetoothMeshService.serviceUUID, primary: true)
service.characteristics = [characteristic]
peripheralManager.add(service)
self.characteristic = characteristic
}
func sendMessage(_ content: String, to recipientID: String? = nil) {
messageQueue.async { [weak self] in
guard let self = self else { return }
let nickname = self.delegate as? ChatViewModel
let senderNick = nickname?.nickname ?? self.myPeerID
let message = BitchatMessage(
sender: senderNick,
content: content,
timestamp: Date(),
isRelay: false,
originalSender: nil
)
if let messageData = try? JSONEncoder().encode(message) {
let packet = BitchatPacket(
type: MessageType.message.rawValue,
senderID: self.myPeerID.data(using: .utf8)!,
recipientID: recipientID?.data(using: .utf8),
timestamp: UInt64(Date().timeIntervalSince1970),
payload: messageData,
signature: try? self.encryptionService.sign(messageData),
ttl: self.maxTTL
)
self.broadcastPacket(packet)
}
}
}
func getPeerNicknames() -> [String: String] {
return peerNicknames
}
private func getAllConnectedPeerIDs() -> [String] {
return Array(activePeers)
}
private func broadcastPacket(_ packet: BitchatPacket) {
guard let data = packet.data else { return }
// Send to connected peripherals (as central)
for (_, peripheral) in connectedPeripherals {
if let characteristic = peripheralCharacteristics[peripheral] {
// Use withResponse for larger data for reliability
let writeType: CBCharacteristicWriteType = data.count > 50000 ? .withResponse : .withoutResponse
peripheral.writeValue(data, for: characteristic, type: writeType)
}
}
// Send to subscribed centrals (as peripheral)
if characteristic != nil && !subscribedCentrals.isEmpty {
peripheralManager.updateValue(data, for: characteristic, onSubscribedCentrals: subscribedCentrals)
}
}
private func handleReceivedPacket(_ packet: BitchatPacket, from peerID: String) {
guard packet.ttl > 0 else { return }
let messageID = "\(packet.timestamp)-\(String(data: packet.senderID, encoding: .utf8) ?? "")"
guard !processedMessages.contains(messageID) else { return }
processedMessages.insert(messageID)
if processedMessages.count > 1000 {
processedMessages.removeAll()
}
switch MessageType(rawValue: packet.type) {
case .message:
if let message = try? JSONDecoder().decode(BitchatMessage.self, from: packet.payload) {
// Ignore our own messages
if let senderID = String(data: packet.senderID, encoding: .utf8), senderID == myPeerID {
return
}
// Store nickname mapping
if let senderID = String(data: packet.senderID, encoding: .utf8) {
peerNicknames[senderID] = message.sender
}
DispatchQueue.main.async {
self.delegate?.didReceiveMessage(message)
}
var relayPacket = packet
relayPacket.ttl -= 1
if relayPacket.ttl > 0 {
self.broadcastPacket(relayPacket)
}
}
case .keyExchange:
if let publicKeyData = try? JSONDecoder().decode(Data.self, from: packet.payload) {
try? encryptionService.addPeerPublicKey(peerID, publicKeyData: publicKeyData)
// Track this peer temporarily but don't announce until we get their name
if peerID != "unknown" && peerID != myPeerID && !peerNicknames.keys.contains(peerID) {
// Just track them, don't announce yet
DispatchQueue.main.async {
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
}
}
// Send announce with our nickname immediately
DispatchQueue.main.async { [weak self] in
if let self = self, let vm = self.delegate as? ChatViewModel {
let announcePacket = BitchatPacket(
type: MessageType.announce.rawValue,
senderID: self.myPeerID.data(using: .utf8)!,
recipientID: peerID.data(using: .utf8),
timestamp: UInt64(Date().timeIntervalSince1970),
payload: vm.nickname.data(using: .utf8)!,
signature: nil,
ttl: 1
)
self.broadcastPacket(announcePacket)
}
}
}
case .announce:
if let nickname = String(data: packet.payload, encoding: .utf8) {
// Store the nickname
peerNicknames[peerID] = nickname
// Add to active peers if not already there
if peerID != "unknown" && peerID != myPeerID && !activePeers.contains(peerID) {
activePeers.insert(peerID)
DispatchQueue.main.async {
self.delegate?.didConnectToPeer(nickname)
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
}
} else {
// Just update the peer list to refresh nicknames
DispatchQueue.main.async {
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
}
}
}
default:
break
}
}
}
extension BluetoothMeshService: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
startScanning()
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
// Connect to any device we discover - we'll filter by service later
if !discoveredPeripherals.contains(peripheral) {
discoveredPeripherals.append(peripheral)
peripheral.delegate = self
central.connect(peripheral, options: nil)
}
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
peripheral.delegate = self
peripheral.discoverServices([BluetoothMeshService.serviceUUID])
// Extract peer ID from advertisement or generate one
var peerID = peripheral.identifier.uuidString.prefix(8).lowercased()
if let name = peripheral.name, name.hasPrefix("bitchat-") {
peerID = String(name.dropFirst(8))
}
connectedPeripherals[String(peerID)] = peripheral
// Update peer list to show we're connecting
DispatchQueue.main.async {
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
}
}
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
if let peerID = connectedPeripherals.first(where: { $0.value == peripheral })?.key {
connectedPeripherals.removeValue(forKey: peerID)
peripheralCharacteristics.removeValue(forKey: peripheral)
// Remove from active peers
activePeers.remove(peerID)
// Only show disconnect if we have a resolved nickname
if let nickname = peerNicknames[peerID], nickname != peerID {
DispatchQueue.main.async {
self.delegate?.didDisconnectFromPeer(nickname)
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
}
} else {
DispatchQueue.main.async {
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
}
}
}
// Remove from discovered list to allow reconnection
discoveredPeripherals.removeAll { $0 == peripheral }
// Continue scanning for reconnection
if centralManager.state == .poweredOn {
// Always restart scan to ensure we can reconnect
centralManager.stopScan()
centralManager.scanForPeripherals(withServices: [BluetoothMeshService.serviceUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false])
}
}
}
extension BluetoothMeshService: CBPeripheralDelegate {
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
guard let services = peripheral.services else { return }
for service in services {
peripheral.discoverCharacteristics([BluetoothMeshService.characteristicUUID], for: service)
}
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
guard let characteristics = service.characteristics else { return }
for characteristic in characteristics {
if characteristic.uuid == BluetoothMeshService.characteristicUUID {
peripheral.setNotifyValue(true, for: characteristic)
peripheralCharacteristics[peripheral] = characteristic
// Wait a moment for subscription to complete before sending data
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
guard let self = self else { return }
let publicKeyData = self.encryptionService.publicKey.rawRepresentation
let packet = BitchatPacket(
type: MessageType.keyExchange.rawValue,
senderID: self.myPeerID.data(using: .utf8)!,
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970),
payload: try! JSONEncoder().encode(publicKeyData),
signature: nil,
ttl: 1
)
if let data = packet.data {
peripheral.writeValue(data, for: characteristic, type: .withResponse)
}
// Also send announce packet immediately
if let vm = self.delegate as? ChatViewModel {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
guard let self = self else { return }
let announcePacket = BitchatPacket(
type: MessageType.announce.rawValue,
senderID: self.myPeerID.data(using: .utf8)!,
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970),
payload: vm.nickname.data(using: .utf8)!,
signature: nil,
ttl: 1
)
if let data = announcePacket.data {
peripheral.writeValue(data, for: characteristic, type: .withResponse)
}
}
}
}
}
}
}
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
guard let data = characteristic.value,
let packet = BitchatPacket.from(data) else {
return
}
let peerID = connectedPeripherals.first(where: { $0.value == peripheral })?.key ?? "unknown"
handleReceivedPacket(packet, from: peerID)
}
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
// Handle write completion if needed
}
func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
peripheral.discoverServices([BluetoothMeshService.serviceUUID])
}
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
// Handle notification state update if needed
}
}
extension BluetoothMeshService: CBPeripheralManagerDelegate {
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
switch peripheral.state {
case .poweredOn:
setupPeripheral()
startAdvertising()
default:
break
}
}
func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
// Handle service addition if needed
}
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
for request in requests {
if let data = request.value,
let packet = BitchatPacket.from(data) {
// Try to identify peer from packet
let peerID = String(data: packet.senderID, encoding: .utf8) ?? "unknown"
// Store the central for updates
if !subscribedCentrals.contains(request.central) {
subscribedCentrals.append(request.central)
}
// Track this peer as connected
if peerID != "unknown" && peerID != myPeerID {
// Send key exchange back if we haven't already
if packet.type == MessageType.keyExchange.rawValue {
let publicKeyData = self.encryptionService.publicKey.rawRepresentation
let responsePacket = BitchatPacket(
type: MessageType.keyExchange.rawValue,
senderID: self.myPeerID.data(using: .utf8)!,
recipientID: peerID.data(using: .utf8),
timestamp: UInt64(Date().timeIntervalSince1970),
payload: try! JSONEncoder().encode(publicKeyData),
signature: nil,
ttl: 1
)
if let data = responsePacket.data {
peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: [request.central])
}
// Also send announce immediately after key exchange
if let vm = self.delegate as? ChatViewModel {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
guard let self = self else { return }
let announcePacket = BitchatPacket(
type: MessageType.announce.rawValue,
senderID: self.myPeerID.data(using: .utf8)!,
recipientID: peerID.data(using: .utf8),
timestamp: UInt64(Date().timeIntervalSince1970),
payload: vm.nickname.data(using: .utf8)!,
signature: nil,
ttl: 1
)
if let data = announcePacket.data {
peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: [request.central])
}
}
}
}
DispatchQueue.main.async {
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
}
}
handleReceivedPacket(packet, from: peerID)
peripheral.respond(to: request, withResult: .success)
}
}
}
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
if !subscribedCentrals.contains(central) {
subscribedCentrals.append(central)
// Send our public key to the newly connected central
let publicKeyData = encryptionService.publicKey.rawRepresentation
let keyPacket = BitchatPacket(
type: MessageType.keyExchange.rawValue,
senderID: myPeerID.data(using: .utf8)!,
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970),
payload: try! JSONEncoder().encode(publicKeyData),
signature: nil,
ttl: 1
)
if let data = keyPacket.data {
peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: [central])
// Also send announce after key exchange
if let vm = delegate as? ChatViewModel {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
guard let self = self else { return }
let announcePacket = BitchatPacket(
type: MessageType.announce.rawValue,
senderID: self.myPeerID.data(using: .utf8)!,
recipientID: nil,
timestamp: UInt64(Date().timeIntervalSince1970),
payload: vm.nickname.data(using: .utf8)!,
signature: nil,
ttl: 1
)
if let data = announcePacket.data {
peripheral.updateValue(data, for: self.characteristic, onSubscribedCentrals: [central])
}
}
}
}
// Update peer list to show we're connected (even without peer ID yet)
DispatchQueue.main.async {
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
}
}
}
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
subscribedCentrals.removeAll { $0 == central }
// If no more centrals are subscribed, clear all central-connected peers
if subscribedCentrals.isEmpty {
// Find and remove peers that were connected as centrals only
let peersToRemove = activePeers.filter { peerID in
!connectedPeripherals.keys.contains(peerID)
}
for peerID in peersToRemove {
activePeers.remove(peerID)
if let nickname = peerNicknames[peerID] {
DispatchQueue.main.async {
self.delegate?.didDisconnectFromPeer(nickname)
}
}
}
DispatchQueue.main.async {
self.delegate?.didUpdatePeerList(self.getAllConnectedPeerIDs())
}
}
// Ensure advertising continues for reconnection
if peripheralManager.state == .poweredOn && !peripheralManager.isAdvertising {
startAdvertising()
}
}
}
+67
View File
@@ -0,0 +1,67 @@
import Foundation
import CryptoKit
class EncryptionService {
private var privateKey: Curve25519.KeyAgreement.PrivateKey
public let publicKey: Curve25519.KeyAgreement.PublicKey
private var peerPublicKeys: [String: Curve25519.KeyAgreement.PublicKey] = [:]
private var sharedSecrets: [String: SymmetricKey] = [:]
init() {
self.privateKey = Curve25519.KeyAgreement.PrivateKey()
self.publicKey = privateKey.publicKey
}
func addPeerPublicKey(_ peerID: String, publicKeyData: Data) throws {
let publicKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: publicKeyData)
peerPublicKeys[peerID] = publicKey
let sharedSecret = try privateKey.sharedSecretFromKeyAgreement(with: publicKey)
let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(
using: SHA256.self,
salt: "bitchat-v1".data(using: .utf8)!,
sharedInfo: Data(),
outputByteCount: 32
)
sharedSecrets[peerID] = symmetricKey
}
func encrypt(_ data: Data, for peerID: String) throws -> Data {
guard let symmetricKey = sharedSecrets[peerID] else {
throw EncryptionError.noSharedSecret
}
let sealedBox = try AES.GCM.seal(data, using: symmetricKey)
return sealedBox.combined ?? Data()
}
func decrypt(_ data: Data, from peerID: String) throws -> Data {
guard let symmetricKey = sharedSecrets[peerID] else {
throw EncryptionError.noSharedSecret
}
let sealedBox = try AES.GCM.SealedBox(combined: data)
return try AES.GCM.open(sealedBox, using: symmetricKey)
}
func sign(_ data: Data) throws -> Data {
let signingKey = try Curve25519.Signing.PrivateKey(rawRepresentation: privateKey.rawRepresentation)
return try signingKey.signature(for: data)
}
func verify(_ signature: Data, for data: Data, from peerID: String) throws -> Bool {
guard let peerPublicKey = peerPublicKeys[peerID] else {
return false
}
let verifyingKey = try Curve25519.Signing.PublicKey(rawRepresentation: peerPublicKey.rawRepresentation)
return verifyingKey.isValidSignature(signature, for: data)
}
}
enum EncryptionError: Error {
case noSharedSecret
case invalidPublicKey
case encryptionFailed
case decryptionFailed
}
+160
View File
@@ -0,0 +1,160 @@
import Foundation
import SwiftUI
import Combine
#if os(iOS)
import UIKit
#endif
class ChatViewModel: ObservableObject {
@Published var messages: [BitchatMessage] = []
@Published var connectedPeers: [String] = []
@Published var nickname: String = "" {
didSet {
nicknameSaveTimer?.invalidate()
nicknameSaveTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
self.saveNickname()
}
}
}
@Published var isConnected = false
let meshService = BluetoothMeshService()
private let userDefaults = UserDefaults.standard
private let nicknameKey = "bitchat.nickname"
private var nicknameSaveTimer: Timer?
init() {
loadNickname()
meshService.delegate = self
// Start mesh service immediately
meshService.startServices()
}
private func loadNickname() {
if let savedNickname = userDefaults.string(forKey: nicknameKey) {
nickname = savedNickname
} else {
nickname = "user\(Int.random(in: 1000...9999))"
saveNickname()
}
}
func saveNickname() {
userDefaults.set(nickname, forKey: nicknameKey)
userDefaults.synchronize() // Force immediate save
}
func sendMessage(_ content: String) {
guard !content.isEmpty else { return }
// Add message to local display
let message = BitchatMessage(
sender: nickname,
content: content,
timestamp: Date(),
isRelay: false,
originalSender: nil
)
messages.append(message)
// Send via mesh
meshService.sendMessage(content)
}
func formatTimestamp(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
return formatter.string(from: date)
}
func formatMessage(_ message: BitchatMessage, colorScheme: ColorScheme) -> AttributedString {
var result = AttributedString()
let isDark = colorScheme == .dark
let primaryColor = isDark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
let secondaryColor = primaryColor.opacity(0.7)
let timestamp = AttributedString("[\(formatTimestamp(message.timestamp))] ")
var timestampStyle = AttributeContainer()
timestampStyle.foregroundColor = secondaryColor
timestampStyle.font = .system(size: 12, design: .monospaced)
result.append(timestamp.mergingAttributes(timestampStyle))
if message.sender == "system" {
let content = AttributedString("* \(message.content) *")
var contentStyle = AttributeContainer()
contentStyle.foregroundColor = secondaryColor
contentStyle.font = .system(size: 12, design: .monospaced).italic()
result.append(content.mergingAttributes(contentStyle))
} else {
let sender = AttributedString("<\(message.sender)> ")
var senderStyle = AttributeContainer()
senderStyle.foregroundColor = message.sender == nickname ? primaryColor : primaryColor.opacity(0.9)
senderStyle.font = .system(size: 12, weight: .medium, design: .monospaced)
result.append(sender.mergingAttributes(senderStyle))
let content = AttributedString(message.content)
var contentStyle = AttributeContainer()
contentStyle.font = .system(size: 14, design: .monospaced)
contentStyle.foregroundColor = isDark ? Color.white : Color.black
result.append(content.mergingAttributes(contentStyle))
if message.isRelay, let originalSender = message.originalSender {
let relay = AttributedString(" (via \(originalSender))")
var relayStyle = AttributeContainer()
relayStyle.foregroundColor = secondaryColor
relayStyle.font = .system(size: 11, design: .monospaced)
result.append(relay.mergingAttributes(relayStyle))
}
}
return result
}
}
extension ChatViewModel: BitchatDelegate {
func didReceiveMessage(_ message: BitchatMessage) {
messages.append(message)
#if os(iOS)
// Haptic feedback for new messages
let impactFeedback = UIImpactFeedbackGenerator(style: .light)
impactFeedback.impactOccurred()
#endif
}
func didConnectToPeer(_ peerID: String) {
isConnected = true
let systemMessage = BitchatMessage(
sender: "system",
content: "\(peerID) has joined",
timestamp: Date(),
isRelay: false,
originalSender: nil
)
messages.append(systemMessage)
}
func didDisconnectFromPeer(_ peerID: String) {
let systemMessage = BitchatMessage(
sender: "system",
content: "\(peerID) has left",
timestamp: Date(),
isRelay: false,
originalSender: nil
)
messages.append(systemMessage)
}
func didUpdatePeerList(_ peers: [String]) {
connectedPeers = peers
isConnected = !peers.isEmpty
// If we just disconnected from all peers, ensure UI updates
if peers.isEmpty && isConnected {
isConnected = false
}
}
}
+177
View File
@@ -0,0 +1,177 @@
import SwiftUI
struct ContentView: View {
@EnvironmentObject var viewModel: ChatViewModel
@State private var messageText = ""
@FocusState private var isTextFieldFocused: Bool
@Environment(\.colorScheme) var colorScheme
@State private var showPeerList = false
private var backgroundColor: Color {
colorScheme == .dark ? Color.black : Color.white
}
private var textColor: Color {
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
}
private var secondaryTextColor: Color {
colorScheme == .dark ? Color.green.opacity(0.8) : Color(red: 0, green: 0.5, blue: 0).opacity(0.8)
}
var body: some View {
VStack(spacing: 0) {
headerView
Divider()
messagesView
Divider()
inputView
}
.background(backgroundColor)
.foregroundColor(textColor)
#if os(macOS)
.frame(minWidth: 600, minHeight: 400)
#endif
}
private var headerView: some View {
HStack {
Text("bitchat")
.font(.system(size: 18, weight: .medium, design: .monospaced))
.foregroundColor(textColor)
Spacer()
Menu {
if viewModel.connectedPeers.isEmpty {
Text("No peers connected")
.font(.system(size: 12, design: .monospaced))
} else {
let peerNicknames = viewModel.meshService.getPeerNicknames()
ForEach(viewModel.connectedPeers, id: \.self) { peerID in
if let displayName = peerNicknames[peerID], displayName != peerID {
// Only show if we have a real nickname
Label(displayName, systemImage: "person.fill")
.font(.system(size: 12, design: .monospaced))
}
}
}
} label: {
HStack(spacing: 4) {
Circle()
.fill(viewModel.isConnected ? textColor : Color.red)
.frame(width: 8, height: 8)
HStack(spacing: 0) {
Text(viewModel.isConnected ? "\(viewModel.connectedPeers.count) \(viewModel.connectedPeers.count == 1 ? "peer" : "peers")" : "Scanning...")
.font(.system(size: 14, design: .monospaced))
.foregroundColor(secondaryTextColor)
Text(Image(systemName: "chevron.down"))
.font(.system(size: 10))
.foregroundColor(secondaryTextColor)
.baselineOffset(-1)
}
}
.contentShape(Rectangle()) // Make entire area tappable
}
.buttonStyle(.plain)
.menuStyle(.borderlessButton)
.menuIndicator(.hidden)
Spacer()
HStack(spacing: 4) {
Text("nick:")
.font(.system(size: 11, design: .monospaced))
.foregroundColor(secondaryTextColor)
TextField("nickname", text: $viewModel.nickname)
.textFieldStyle(.plain)
.font(.system(size: 12, design: .monospaced))
.frame(maxWidth: 100)
.foregroundColor(textColor)
.onChange(of: viewModel.nickname) { _ in
viewModel.saveNickname()
}
.onSubmit {
viewModel.saveNickname()
}
}
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(backgroundColor.opacity(0.95))
}
private var messagesView: some View {
ScrollViewReader { proxy in
ScrollView {
LazyVStack(alignment: .leading, spacing: 2) {
ForEach(Array(viewModel.messages.enumerated()), id: \.offset) { index, message in
VStack(alignment: .leading, spacing: 4) {
Text(viewModel.formatMessage(message, colorScheme: colorScheme))
.font(.system(size: 14, design: .monospaced))
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.horizontal, 12)
.padding(.vertical, 2)
.id(index)
}
}
.padding(.vertical, 8)
}
.background(backgroundColor)
.onChange(of: viewModel.messages.count) { _ in
withAnimation {
proxy.scrollTo(viewModel.messages.count - 1, anchor: .bottom)
}
}
}
}
private var inputView: some View {
HStack(spacing: 4) {
Text("[\(viewModel.formatTimestamp(Date()))]")
.font(.system(size: 12, design: .monospaced))
.foregroundColor(secondaryTextColor)
.lineLimit(1)
.fixedSize()
.padding(.leading, 12)
Text("<\(viewModel.nickname)>")
.font(.system(size: 12, weight: .medium, design: .monospaced))
.foregroundColor(textColor)
.lineLimit(1)
.fixedSize()
TextField("", text: $messageText)
.textFieldStyle(.plain)
.font(.system(size: 14, design: .monospaced))
.foregroundColor(textColor)
.focused($isTextFieldFocused)
.onSubmit {
sendMessage()
}
Button(action: sendMessage) {
Image(systemName: "arrow.right.circle.fill")
.font(.system(size: 16))
.foregroundColor(textColor)
}
.buttonStyle(.plain)
.padding(.trailing, 12)
}
.padding(.vertical, 10)
.background(backgroundColor.opacity(0.95))
.onAppear {
isTextFieldFocused = true
}
}
private func sendMessage() {
viewModel.sendMessage(messageText)
messageText = ""
}
}
+46
View File
@@ -0,0 +1,46 @@
name: bitchat
options:
bundleIdPrefix: com.bitchat
deploymentTarget:
iOS: 16.0
macOS: 13.0
createIntermediateGroups: true
settings:
MARKETING_VERSION: 1.0.0
CURRENT_PROJECT_VERSION: 1
DEVELOPMENT_TEAM: ""
targets:
bitchat:
type: application
platform: [iOS, macOS]
sources:
- bitchat
info:
path: bitchat/Info.plist
properties:
CFBundleDisplayName: bitchat
CFBundleShortVersionString: $(MARKETING_VERSION)
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
LSMinimumSystemVersion: $(MACOSX_DEPLOYMENT_TARGET)
NSBluetoothAlwaysUsageDescription: bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users.
NSBluetoothPeripheralUsageDescription: bitchat uses Bluetooth to discover and connect with other bitchat users nearby.
UILaunchScreen:
UIColorName: Black
UISupportedInterfaceOrientations:
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
settings:
PRODUCT_BUNDLE_IDENTIFIER: com.bitchat.app
INFOPLIST_FILE: bitchat/Info.plist
ENABLE_PREVIEWS: YES
SWIFT_VERSION: 5.0
IPHONEOS_DEPLOYMENT_TARGET: 16.0
MACOSX_DEPLOYMENT_TARGET: 13.0
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: YES
CODE_SIGN_STYLE: Automatic
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS: YES
Executable
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
echo "bitchat Setup Script"
echo "==================="
# Check if XcodeGen is installed
if command -v xcodegen &> /dev/null; then
echo "✓ XcodeGen found"
echo "Generating Xcode project..."
xcodegen generate
echo "✓ Project generated successfully"
echo ""
echo "To open the project, run:"
echo " open bitchat.xcodeproj"
else
echo "⚠️ XcodeGen not found"
echo ""
echo "You have several options:"
echo "1. Install XcodeGen:"
echo " brew install xcodegen"
echo ""
echo "2. Open with Swift Package Manager:"
echo " open Package.swift"
echo ""
echo "3. Create a new Xcode project manually and add the source files"
fi
echo ""
echo "Project Structure:"
echo "- bitchat/ Main source files"
echo " - BitchatApp.swift App entry point"
echo " - Views/ SwiftUI views"
echo " - ViewModels/ View models"
echo " - Services/ Bluetooth and encryption"
echo " - Protocols/ Protocol definitions"
echo ""
echo "Remember to:"
echo "1. Enable Bluetooth in device settings"
echo "2. Run on physical devices (Bluetooth doesn't work in simulator)"
echo "3. Test with multiple devices for mesh functionality"