Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# ValidationRelay

ValidationRelay is an iOS companion used by OpenBubbles to keep the Apple-side
validation channel available. It is a relay component, not an iMessage client:
the Android app still owns the user interface and message persistence, while
this app provides device identifiers and validation data over its WebSocket
connection.

## Repository status and licensing

This checkout does not currently contain a declared repository license. Do not
redistribute binaries or publish a fork until the project owner confirms the
license and the terms for the Apple/TrollStore integration. Contributions may
still be prepared as reviewable patches, but licensing is a release gate.

## Build requirements

- macOS with Xcode and the iOS SDK
- An iOS device that can install the resulting app (the project deployment
settings currently include iOS 14 and newer)
- Swift Package Manager access to `pusher/NWWebSocket` 0.5.10 or newer within
the declared major range
- For the TrollStore path, the repository's `TrollStore/` submodule and its
documented signing tools

The checked-in `build.sh` produces an unsigned device build and then packages a
`.tipa` using `ldid` for the normal path. `./build.sh --embed` additionally
builds the embedded fastPathSign helper. Review the script before running it on
a new machine because signing and installation are intentionally environment-
specific.

## Runtime architecture

`RelayConnectionManager` owns one active WebSocket delegate, reconnect backoff,
connection timeout, foreground health checks, and the watchdog. Registration
code and secret are treated as a pair. The secret is stored in Keychain when
available, with a legacy local-storage migration only for existing installs.

Validation generation runs on a dedicated serial queue. Apple certificate and
validation requests use HTTPS, bounded URL-session timeouts, throwing plist
decoders, and native-operation status checks. The UI receives a success or
failure result on the main queue.

## Safe testing checklist

1. Use a test Apple account and a non-production relay where possible.
2. Keep the relay app foregrounded and the phone awake for initial testing.
3. Test connect, reconnect, network change, foreground resume, malformed
registration response, and validation failure separately.
4. Confirm that one connection manager and one reconnect timer are active.
5. Check the event log for operation names and redacted IDs only.
6. Never paste registration secrets, Apple credentials, device identifiers, or
validation payloads into an issue.

An apparent WebSocket “connected” state is not sufficient evidence of a usable
relay. The registration response, validation request, and downstream message
acknowledgement must each succeed.

## Field-validated configuration

The following workflow has been exercised successfully on one concrete
configuration:

- macOS/Xcode compilation of the iOS target
- Signing and installation on an iPhone 7 through TrollStore/ImmortalizerTS
- Apple validation-data exchange
- Carrier registration and physical-device relay behavior
- Foreground/Keep Awake behavior, plus locked-phone and display-off endurance
behavior on a non-jailbroken TrollStore device, with ImmortalizerTS working
and the relay remaining operational
- Device: iPhone 7 running iOS 15.7.9

This is field validation of the checked configuration, not a universal
compatibility guarantee for every iOS release, carrier, signing environment, or
device model. Future release candidates should repeat the locked-phone,
display-off, foreground, reconnect, and network-change endurance tests. iOS
suspension and background execution remain OS- and device-dependent, even when
this setup is working correctly.

## Known limitations

- Windows-side tooling can capture iPhone Bluetooth traffic for diagnostics,
but it does not expose raw Apple UWB frames or private Find My session keys.
- A phone can be connected to the relay while Apple validation is failing due
to certificate, network, account, or native `absd` errors.
- Background execution and network path migration remain device/OS dependent;
the app cannot guarantee an always-on WebSocket when iOS suspends it.
- Xcode compilation and a long locked-device endurance test were validated for
the iPhone 7/iOS 15.7.9 setup above, but remain required again for future
release candidates and other device/OS combinations.

For Android-client routing and redacted receive diagnostics, see the
corresponding `docs/DEVELOPMENT.md` and `docs/DIAGNOSTICS.md` files in the
OpenBubbles Android client checkout.
10 changes: 5 additions & 5 deletions ValidationRelay.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = ValidationRelay/ValidationRelay.entitlements;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_ASSET_PATHS = "\"ValidationRelay/Preview Content\"";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
Expand All @@ -335,7 +335,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.8;
MARKETING_VERSION = 1.8.2;
PRODUCT_BUNDLE_IDENTIFIER = dev.jjtech.experiments.ValidationRelay;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
Expand All @@ -357,7 +357,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = ValidationRelay/ValidationRelay.entitlements;
CURRENT_PROJECT_VERSION = 1;
CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_ASSET_PATHS = "\"ValidationRelay/Preview Content\"";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
Expand All @@ -374,7 +374,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.8;
MARKETING_VERSION = 1.8.2;
PRODUCT_BUNDLE_IDENTIFIER = dev.jjtech.experiments.ValidationRelay;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
Expand Down Expand Up @@ -417,7 +417,7 @@
repositoryURL = "https://github.com/pusher/NWWebSocket.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.5.4;
minimumVersion = 0.5.10;
};
};
/* End XCRemoteSwiftPackageReference section */
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 38 additions & 22 deletions ValidationRelay/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,42 @@
import SwiftUI

struct ContentView: View {
@Environment(\.scenePhase) private var scenePhase
@AppStorage("autoConnect") private var wantRelayConnected = true
@AppStorage("keepAwake") private var keepAwake = true

@AppStorage("selectedRelay") private var selectedRelay = "Beeper"
@AppStorage("customRelayURL") private var customRelayURL = ""

@ObservedObject var relayConnectionManager: RelayConnectionManager

init(relayConnectionManager: RelayConnectionManager) {
self.relayConnectionManager = relayConnectionManager
if wantRelayConnected {
relayConnectionManager.connect(getCurrentRelayURL())
}
if keepAwake {
UIApplication.shared.isIdleTimerDisabled = true
}
}

func getCurrentRelayURL() -> URL {

func getCurrentRelayURL() -> URL? {
if selectedRelay == "Custom" {
if let url = URL(string: customRelayURL) {
return url
guard let components = URLComponents(string: customRelayURL.trimmingCharacters(in: .whitespacesAndNewlines)),
components.scheme?.lowercased() == "wss",
components.host?.isEmpty == false,
components.user == nil,
components.password == nil,
components.percentEncodedQuery == nil,
components.fragment == nil else {
return nil
}
return components.url
} else if selectedRelay == "pypush" {
return URL(string: "wss://registration-relay.jjtech.dev/api/v1/provider")!
}

// Default to Beeper relay
selectedRelay = "Beeper"
return URL(string: "wss://registration-relay.beeper.com/api/v1/provider")!
}

func connectCurrentRelay() {
guard let url = getCurrentRelayURL() else {
relayConnectionManager.connectionStatusMessage = "Custom relay must be a secure wss:// URL without embedded credentials or a query."
return
}
relayConnectionManager.connectIfNeeded(url)
}

var body: some View {
NavigationView {
Expand All @@ -48,7 +53,7 @@ struct ContentView: View {
.onChange(of: wantRelayConnected) { newValue in
// Connect or disconnect the relay
if newValue {
relayConnectionManager.connect(getCurrentRelayURL())
connectCurrentRelay()
} else {
relayConnectionManager.disconnect()
}
Expand Down Expand Up @@ -99,11 +104,7 @@ struct ContentView: View {
}
Toggle("Keep Awake", isOn: $keepAwake)
.onChange(of: keepAwake) { newValue in
if keepAwake {
UIApplication.shared.isIdleTimerDisabled = true
} else {
UIApplication.shared.isIdleTimerDisabled = false
}
UIApplication.shared.isIdleTimerDisabled = newValue
}
Button("Reset Registration Code") {
relayConnectionManager.savedRegistrationURL = ""
Expand All @@ -115,12 +116,27 @@ struct ContentView: View {
.foregroundColor(.red)
.frame(maxWidth: .infinity)
} footer: {
Text("You will need to re-enter the code on your other devices")
Text("You will need to re-enter the code on your other devices. Keep ValidationRelay open in the foreground and keep this iPhone awake.")
}
}
.listStyle(.grouped)
.navigationBarHidden(true)
.navigationBarTitle("", displayMode: .inline)
.onAppear {
UIApplication.shared.isIdleTimerDisabled = keepAwake
if wantRelayConnected {
connectCurrentRelay()
}
}
.onChange(of: scenePhase) { newPhase in
guard newPhase == .active,
wantRelayConnected,
let url = getCurrentRelayURL() else {
return
}
UIApplication.shared.isIdleTimerDisabled = keepAwake
relayConnectionManager.refreshOnForeground(url)
}
}

}
Expand Down
4 changes: 0 additions & 4 deletions ValidationRelay/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,5 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIBackgroundModes</key>
<array>
<string>processing</string>
</array>
</dict>
</plist>
4 changes: 4 additions & 0 deletions ValidationRelay/LogView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ struct LogItem: Identifiable, Hashable {
}

class LogItems: ObservableObject {
private let maximumItemCount = 500
@Published var items: [LogItem] = []

func log(_ message: String, isError: Bool = false) {
let item = LogItem(message: message, isError: isError, date: Date())
items.append(item)
if items.count > maximumItemCount {
items.removeFirst(items.count - maximumItemCount)
}
}
}

Expand Down
Loading