mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-13 20:25:53 +08:00
[feat] add iOS trusted device authentication
This commit is contained in:
1 parent
94a5f57bea
commit
8a1e58d62d
6 files changed
+136
-3
No files matched your search
@@ -151,6 +151,7 @@ typedef struct {
|
||||
/// All MiniRTC ownership and calls are serialized internally. Delegate methods
|
||||
/// are delivered on the main queue so SwiftUI never touches callback threads.
|
||||
@interface CrossDeskRTCBridge : NSObject
|
||||
+ (nullable NSDictionary<NSString *, NSString *> *)deviceTrustInfoForDeviceID:(NSString *)deviceID;
|
||||
|
||||
@property(nonatomic, weak, nullable) id<CrossDeskRTCBridgeDelegate> delegate;
|
||||
|
||||
|
||||
@@ -23,9 +23,12 @@
|
||||
#include <remote_action.h>
|
||||
#include <stream_names.h>
|
||||
#include "minirtc.h"
|
||||
#include "../../../../libs/controller/include/mobile_device_trust.h"
|
||||
|
||||
namespace {
|
||||
|
||||
using crossdesk::mobile_controller::DeviceTrustIdentity;
|
||||
|
||||
using crossdesk::ControlType;
|
||||
using crossdesk::FileTransferAck;
|
||||
using crossdesk::KeyFlag;
|
||||
@@ -71,6 +74,7 @@ struct PresenceRequest {
|
||||
};
|
||||
|
||||
struct RTCState {
|
||||
std::unique_ptr<DeviceTrustIdentity> trust;
|
||||
MiniRtcPeerPtr *identity_peer = nullptr;
|
||||
MiniRtcPeerPtr *controller_peer = nullptr;
|
||||
CallbackContext identity_context;
|
||||
@@ -211,6 +215,47 @@ OSStatus RemoveIdentityCredential(NSString *account) {
|
||||
return status;
|
||||
}
|
||||
|
||||
std::unique_ptr<DeviceTrustIdentity> LoadDeviceTrustIdentity() {
|
||||
// Pairing export and all bridges share one installation-wide Keychain item.
|
||||
static std::mutex mutex;
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
NSDictionary *query = @{
|
||||
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
||||
(__bridge id)kSecAttrService: @"cn.crossdesk.mobile.device-trust",
|
||||
(__bridge id)kSecAttrAccount: @"v1",
|
||||
(__bridge id)kSecAttrSynchronizable: @NO,
|
||||
};
|
||||
for (int attempt = 0; attempt < 2; ++attempt) {
|
||||
NSMutableDictionary *read = [query mutableCopy];
|
||||
read[(__bridge id)kSecReturnData] = @YES;
|
||||
read[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
|
||||
CFTypeRef result = nullptr;
|
||||
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)read, &result);
|
||||
id object = CFBridgingRelease(result);
|
||||
std::string seed;
|
||||
struct Clean { std::string& value; ~Clean() { DeviceTrustIdentity::ClearSecret(value); } } clean{seed};
|
||||
if (status == errSecSuccess && [object isKindOfClass:NSData.class]) {
|
||||
NSData *data = object;
|
||||
seed.assign(static_cast<const char *>(data.bytes), data.length);
|
||||
} else if (status == errSecItemNotFound &&
|
||||
![NSUserDefaults.standardUserDefaults boolForKey:@"CrossDeskDeviceTrustInitialized.v1"]) {
|
||||
seed = DeviceTrustIdentity::GeneratePrivateKey();
|
||||
NSMutableDictionary *item = [query mutableCopy];
|
||||
item[(__bridge id)kSecValueData] = [NSData dataWithBytes:seed.data() length:seed.size()];
|
||||
item[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
|
||||
status = SecItemAdd((__bridge CFDictionaryRef)item, nullptr);
|
||||
if (status == errSecDuplicateItem) continue;
|
||||
if (status != errSecSuccess) throw std::runtime_error("Device signing identity save failed");
|
||||
} else {
|
||||
throw std::runtime_error("Device signing identity unavailable");
|
||||
}
|
||||
auto identity = std::make_unique<DeviceTrustIdentity>(seed);
|
||||
[NSUserDefaults.standardUserDefaults setBool:YES forKey:@"CrossDeskDeviceTrustInitialized.v1"];
|
||||
return identity;
|
||||
}
|
||||
throw std::runtime_error("Device signing identity unavailable");
|
||||
}
|
||||
|
||||
void CopyCString(char *destination, size_t capacity,
|
||||
const std::string &source) {
|
||||
if (!destination || capacity == 0) return;
|
||||
@@ -499,6 +544,15 @@ MiniRtcParams MakeParams(const RTCState &state, const std::string &user_id,
|
||||
std::atomic<uint64_t> _networkingGeneration;
|
||||
}
|
||||
|
||||
+ (nullable NSDictionary<NSString *, NSString *> *)deviceTrustInfoForDeviceID:(NSString *)deviceID {
|
||||
try {
|
||||
auto trust = LoadDeviceTrustIdentity();
|
||||
auto pairing = trust->PairingInfo(BaseIdentity(deviceID.UTF8String ?: ""), "CrossDesk iOS");
|
||||
return @{@"pairing": [NSString stringWithUTF8String:pairing.c_str()],
|
||||
@"fingerprint": [NSString stringWithUTF8String:trust->Fingerprint().c_str()]};
|
||||
} catch (const std::exception&) { return nil; }
|
||||
}
|
||||
|
||||
+ (nullable NSString *)availableUpdateForAppVersion:(NSString *)appVersion
|
||||
releaseJSON:(NSData *)releaseJSON {
|
||||
const auto latest = crossdesk::ParseVersionInfoJSON(std::string_view(
|
||||
@@ -610,6 +664,15 @@ MiniRtcParams MakeParams(const RTCState &state, const std::string &user_id,
|
||||
self->_state->identity_with_password.clear();
|
||||
self->_state->identity_base.clear();
|
||||
|
||||
try {
|
||||
self->_state->trust = LoadDeviceTrustIdentity();
|
||||
} catch (const std::exception&) {
|
||||
[self reportIdentityStorageStatus:errSecDecode];
|
||||
[self handleSignalState:CrossDeskSignalStateCredentialUnavailable
|
||||
role:PeerRole::Identity generation:self->_identityGeneration.load()];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *cached = nil;
|
||||
OSStatus load_status = LoadIdentityCredential(
|
||||
IdentityCredentialAccount(trimmed, signalPort), &cached);
|
||||
@@ -1484,7 +1547,7 @@ MiniRtcParams MakeParams(const RTCState &state, const std::string &user_id,
|
||||
action.i.top = nullptr;
|
||||
action.i.right = nullptr;
|
||||
action.i.bottom = nullptr;
|
||||
const std::string host_info = action.to_json();
|
||||
const std::string host_info = DeviceTrustIdentity::Advertise(action.to_json());
|
||||
MiniRtcSendReliableDataFrame(self->_state->controller_peer, host_info.data(),
|
||||
host_info.size(), kControlStream);
|
||||
const std::string stream = crossdesk::MakeDisplayStreamId(
|
||||
@@ -1754,6 +1817,18 @@ MiniRtcParams MakeParams(const RTCState &state, const std::string &user_id,
|
||||
if (!data || size == 0 || !sourceID) return;
|
||||
const std::string source(sourceID, sourceIDSize);
|
||||
if (source == kControlStream) {
|
||||
if (DeviceTrustIdentity::IsChallenge(std::string_view(data, size))) {
|
||||
const std::string challenge(data, size);
|
||||
dispatch_async(_rtcQueue, ^{
|
||||
if (![self isControllerGenerationActive:generation] || !self->_state->controller_peer ||
|
||||
!self->_state->trust) return;
|
||||
auto proof = self->_state->trust->ChallengeResponse(challenge,
|
||||
self->_state->identity_base, "CrossDesk iOS");
|
||||
if (proof && !proof->empty())
|
||||
MiniRtcSendReliableDataFrame(self->_state->controller_peer, proof->data(), proof->size(), kControlStream);
|
||||
});
|
||||
return;
|
||||
}
|
||||
RemoteAction action{};
|
||||
if (!action.from_json(std::string(data, size))) return;
|
||||
if (action.type == ControlType::video_codec_status) {
|
||||
|
||||
@@ -804,6 +804,37 @@ private struct PasswordPromptView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct DevicePairingView: View {
|
||||
let deviceID: String
|
||||
@State private var info: [String: String]?
|
||||
@State private var copied = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Text(NSLocalizedString("将配对信息添加到被控端的可信设备列表,并核对指纹。", comment: ""))
|
||||
if let info, let fingerprint = info["fingerprint"], let pairing = info["pairing"] {
|
||||
Text(fingerprint.enumerated().map { index, character in
|
||||
(index > 0 && index % 4 == 0 ? " " : "") + String(character)
|
||||
}.joined()).font(.footnote.monospaced()).textSelection(.enabled)
|
||||
Button(NSLocalizedString("复制配对信息", comment: "")) {
|
||||
UIPasteboard.general.setItems([["public.utf8-plain-text": pairing]], options: [.localOnly: true])
|
||||
copied = true
|
||||
}
|
||||
if copied { Text(NSLocalizedString("配对信息已复制。", comment: "")).foregroundStyle(.secondary) }
|
||||
} else {
|
||||
Text(NSLocalizedString("安全存储不可用,请解锁设备后重试", comment: "")).foregroundStyle(.orange)
|
||||
Button(NSLocalizedString("重试", comment: "")) { load() }
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(NSLocalizedString("本机配对信息", comment: ""))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.task { load() }
|
||||
}
|
||||
private func load() { info = CrossDeskRTCBridge.deviceTrustInfo(forDeviceID: deviceID); copied = false }
|
||||
}
|
||||
|
||||
private struct ServerSettingsView: View {
|
||||
@EnvironmentObject private var appUpdates: AppUpdateChecker
|
||||
@ObservedObject var session: RemoteSessionModel
|
||||
@@ -901,6 +932,11 @@ private struct ServerSettingsView: View {
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
PrivacySettingsSection(session: session)
|
||||
Section {
|
||||
NavigationLink(NSLocalizedString("本机配对信息", comment: "")) {
|
||||
DevicePairingView(deviceID: session.localIdentity)
|
||||
}
|
||||
}
|
||||
Section {
|
||||
NavigationLink {
|
||||
ThirdPartyLicensesView(session: session)
|
||||
|
||||
@@ -8,6 +8,12 @@ UPnP discovery and router port mapping are disabled in iOS builds, matching
|
||||
Android. Local connections, ICE/STUN hole punching, and TURN UDP/TCP relay
|
||||
remain available. The native archive excludes GUPnP and its HTTP/XML dependencies.
|
||||
|
||||
## Trusted device authentication
|
||||
|
||||
Open **Settings → This device’s pairing info** to view the fingerprint and copy the public pairing information. Import it under **Manage trusted devices → Add a trusted device** on the controlled desktop and compare fingerprints. Once trusted, this iPhone or iPad can connect with **Only allow trusted devices** enabled. The connection password is still required.
|
||||
|
||||
The controller responds to the shared Ed25519 challenge protocol. Its signing seed is stored in a separate, non-synchronizing, device-only Keychain item, never in caches, logs or the exported pairing information. Restarting, clearing caches and changing signaling servers or login IDs preserve the same key. An unavailable, corrupt or unexpectedly missing identity fails initialization instead of silently generating a replacement. A new installation on another device needs its own pairing.
|
||||
|
||||
## Remote privacy screen
|
||||
|
||||
The floating menu includes a **Privacy screen** button that toggles the host's
|
||||
|
||||
@@ -155,6 +155,7 @@ WIRE_BUILD_DIR="${IOS_DIR}/.xmake/wire-build${CACHE_SUFFIX}"
|
||||
WIRE_WORK_DIR="${IOS_DIR}/.xmake/wire-work${CACHE_SUFFIX}"
|
||||
WIRE_MANIFEST_FILE="${WIRE_WORK_DIR}/xmake.lua"
|
||||
WIRE_LIBRARY="${WIRE_BUILD_DIR}/iphoneos/arm64/${MODE}/libcrossdesk_wire.a"
|
||||
TRUST_LIBRARY="${WIRE_BUILD_DIR}/iphoneos/arm64/${MODE}/libcrossdesk_mobile_trust.a"
|
||||
|
||||
mkdir -p "${OUTPUT_DIR}" "${WIRE_WORK_DIR}"
|
||||
if ! cmp -s "${REPO_DIR}/xmake.lua" "${WIRE_MANIFEST_FILE}"; then
|
||||
@@ -228,6 +229,7 @@ fi
|
||||
cd "${WIRE_WORK_DIR}"
|
||||
print "Building CrossDesk wire for ${SDK_NAME} (${MODE})"
|
||||
run_xmake b -P "${WIRE_WORK_DIR}" "${XMAKE_LOG_FLAGS[@]}" crossdesk_wire
|
||||
run_xmake b -P "${WIRE_WORK_DIR}" "${XMAKE_LOG_FLAGS[@]}" crossdesk_mobile_trust
|
||||
)
|
||||
|
||||
if [[ ! -f "${WIRE_LIBRARY}" ]]; then
|
||||
@@ -304,7 +306,7 @@ verify_native_symbols() {
|
||||
# timestamp tick cannot leave a stale merged library. Preserve the output mtime
|
||||
# on no-op builds to avoid relinking the Xcode app.
|
||||
shasum -a 256 "${SCRIPT_DIR}/build_minirtc_ios.sh" "${MINIRTC_LIBRARY}" \
|
||||
"${DEPENDENCY_ARCHIVES[@]}" "${WIRE_LIBRARY}" > "${TEMP_MANIFEST}"
|
||||
"${DEPENDENCY_ARCHIVES[@]}" "${WIRE_LIBRARY}" "${TRUST_LIBRARY}" > "${TEMP_MANIFEST}"
|
||||
if [[ -f "${OUTPUT_LIBRARY}" ]] && cmp -s "${TEMP_MANIFEST}" "${MERGE_MANIFEST}"; then
|
||||
verify_native_symbols "${OUTPUT_LIBRARY}"
|
||||
print "Up to date: ${OUTPUT_LIBRARY}"
|
||||
@@ -315,7 +317,7 @@ fi
|
||||
# Suppress only that diagnostic; retain the members and other warnings.
|
||||
/usr/bin/libtool -static -no_warning_for_no_symbols -o "${TEMP_LIBRARY}" \
|
||||
"${MINIRTC_LIBRARY}" "${MERGE_ARCHIVES[@]}" \
|
||||
"${WIRE_LIBRARY}"
|
||||
"${WIRE_LIBRARY}" "${TRUST_LIBRARY}"
|
||||
verify_native_symbols "${TEMP_LIBRARY}"
|
||||
mv -f "${TEMP_LIBRARY}" "${OUTPUT_LIBRARY}"
|
||||
mv -f "${TEMP_MANIFEST}" "${MERGE_MANIFEST}"
|
||||
|
||||
Reference in new issue
Block a user