[fix] improve iOS privacy and App Store distribution compliance

This commit is contained in:
dijunkun
2026-09-28 10:23:41 +08:00
parent e894f4a763
commit 0b28d1bc99
21 changed files with 2848 additions and 58 deletions
@@ -15,6 +15,11 @@
100000000000000000000006 /* NativeMetalVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000009 /* NativeMetalVideoView.swift */; };
100000000000000000000007 /* RemoteTouchInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20000000000000000000000A /* RemoteTouchInputView.swift */; };
100000000000000000000008 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 20000000000000000000000D /* Assets.xcassets */; };
100000000000000000000009 /* ThirdPartyLicensesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20000000000000000000000E /* ThirdPartyLicensesView.swift */; };
10000000000000000000000A /* ThirdPartyLicenses.json in Resources */ = {isa = PBXBuildFile; fileRef = 20000000000000000000000F /* ThirdPartyLicenses.json */; };
10000000000000000000000B /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000010 /* PrivacyInfo.xcprivacy */; };
10000000000000000000000C /* OpenSSL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 200000000000000000000011 /* OpenSSL.framework */; };
10000000000000000000000D /* OpenSSL.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 200000000000000000000011 /* OpenSSL.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
@@ -31,13 +36,30 @@
20000000000000000000000B /* CrossDeskMobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CrossDeskMobile.app; sourceTree = BUILT_PRODUCTS_DIR; };
20000000000000000000000C /* build_minirtc_ios.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = build_minirtc_ios.sh; sourceTree = "<group>"; };
20000000000000000000000D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
20000000000000000000000E /* ThirdPartyLicensesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartyLicensesView.swift; sourceTree = "<group>"; };
20000000000000000000000F /* ThirdPartyLicenses.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Resources/ThirdPartyLicenses.json; sourceTree = "<group>"; };
200000000000000000000010 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
200000000000000000000011 /* OpenSSL.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OpenSSL.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXCopyFilesBuildPhase section */
300000000000000000000002 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (10000000000000000000000D /* OpenSSL.framework in Embed Frameworks */, );
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFrameworksBuildPhase section */
300000000000000000000001 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
10000000000000000000000C /* OpenSSL.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -59,6 +81,9 @@
200000000000000000000001 /* CrossDeskMobileApp.swift */,
200000000000000000000002 /* RemoteSessionModel.swift */,
200000000000000000000003 /* ContentView.swift */,
20000000000000000000000E /* ThirdPartyLicensesView.swift */,
20000000000000000000000F /* ThirdPartyLicenses.json */,
200000000000000000000010 /* PrivacyInfo.xcprivacy */,
200000000000000000000004 /* RemoteSessionView.swift */,
400000000000000000000003 /* Bridge */,
400000000000000000000004 /* Rendering */,
@@ -107,6 +132,7 @@
isa = PBXGroup;
children = (
20000000000000000000000B /* CrossDeskMobile.app */,
200000000000000000000011 /* OpenSSL.framework */,
);
name = Products;
sourceTree = "<group>";
@@ -122,6 +148,7 @@
600000000000000000000001 /* Sources */,
300000000000000000000001 /* Frameworks */,
600000000000000000000002 /* Resources */,
300000000000000000000002 /* Embed Frameworks */,
);
buildRules = (
);
@@ -172,6 +199,8 @@
buildActionMask = 2147483647;
files = (
100000000000000000000008 /* Assets.xcassets in Resources */,
10000000000000000000000A /* ThirdPartyLicenses.json in Resources */,
10000000000000000000000B /* PrivacyInfo.xcprivacy in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -193,10 +222,16 @@
);
outputPaths = (
"$(PROJECT_DIR)/Vendor/iphoneos/$(CONFIGURATION)/libCrossDeskMiniRTC.a",
"$(BUILT_PRODUCTS_DIR)/OpenSSL.framework/OpenSSL",
"$(BUILT_PRODUCTS_DIR)/OpenSSL.framework/Info.plist",
"$(BUILT_PRODUCTS_DIR)/OpenSSL.framework/PrivacyInfo.xcprivacy",
"$(BUILT_PRODUCTS_DIR)/OpenSSL.framework/LICENSE.txt",
"$(BUILT_PRODUCTS_DIR)/OpenSSL.framework/BuildMetadata.json",
"$(TARGET_BUILD_DIR)/$(UNLOCALIZED_RESOURCES_FOLDER_PATH)/SourceMetadata.json",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/zsh;
shellScript = "\"${PROJECT_DIR}/scripts/build_minirtc_ios.sh\"\n";
shellScript = "set -e\npython3 \"${PROJECT_DIR}/scripts/prepare_source_metadata.py\" --output \"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SourceMetadata.json\"\n\"${PROJECT_DIR}/scripts/build_minirtc_ios.sh\"\npython3 \"${PROJECT_DIR}/scripts/generate_third_party_licenses.py\" --inputs \"${PROJECT_DIR}/Vendor/iphoneos/${CONFIGURATION}/libCrossDeskMiniRTC.a.inputs.sha256\" --target-info \"${PROJECT_DIR}/Vendor/iphoneos/${CONFIGURATION}/minirtc-target-info.txt\" --xmake-version \"${PROJECT_DIR}/Vendor/iphoneos/${CONFIGURATION}/xmake-version.txt\" --xmake-repository \"$(cat \"${PROJECT_DIR}/Vendor/iphoneos/${CONFIGURATION}/xmake-repository.txt\")\"\npython3 \"${PROJECT_DIR}/scripts/package_openssl_ios.py\" --inputs \"${PROJECT_DIR}/Vendor/iphoneos/${CONFIGURATION}/libCrossDeskMiniRTC.a.inputs.sha256\" --output \"${BUILT_PRODUCTS_DIR}/OpenSSL.framework\"\n";
};
/* End PBXShellScriptBuildPhase section */
@@ -208,6 +243,7 @@
100000000000000000000001 /* CrossDeskMobileApp.swift in Sources */,
100000000000000000000002 /* RemoteSessionModel.swift in Sources */,
100000000000000000000003 /* ContentView.swift in Sources */,
100000000000000000000009 /* ThirdPartyLicensesView.swift in Sources */,
100000000000000000000004 /* RemoteSessionView.swift in Sources */,
100000000000000000000005 /* CrossDeskRTCBridge.mm in Sources */,
100000000000000000000006 /* NativeMetalVideoView.swift in Sources */,
@@ -270,6 +306,7 @@
ENABLE_BITCODE = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = NO;
FRAMEWORK_SEARCH_PATHS = ("$(inherited)", "$(BUILT_PRODUCTS_DIR)", );
HEADER_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/../../deps/submodules/minirtc/src/api",
@@ -319,6 +356,7 @@
ENABLE_BITCODE = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = NO;
FRAMEWORK_SEARCH_PATHS = ("$(inherited)", "$(BUILT_PRODUCTS_DIR)", );
HEADER_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/../../deps/submodules/minirtc/src/api",
@@ -11,6 +11,7 @@ typedef NS_ENUM(NSInteger, CrossDeskSignalState) {
CrossDeskSignalStateReconnecting,
CrossDeskSignalStateServerClosed,
CrossDeskSignalStateTLSCertificateError,
CrossDeskSignalStateCredentialUnavailable,
};
typedef NS_ENUM(NSInteger, CrossDeskConnectionState) {
@@ -47,6 +48,8 @@ typedef NS_ENUM(NSInteger, CrossDeskVideoAdaptationPolicy) {
@optional
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didChangeSignalState:(CrossDeskSignalState)state;
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didChangeIdentityStorageError:(BOOL)hasError;
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didChangeConnectionState:(CrossDeskConnectionState)state
remoteID:(NSString *)remoteID;
@@ -1,6 +1,7 @@
#import "CrossDeskRTCBridge.h"
#import <CoreMedia/CoreMedia.h>
#import <Security/Security.h>
#import <UIKit/UIKit.h>
#include <algorithm>
@@ -101,6 +102,113 @@ bool IsReusableIdentity(const std::string &identity) {
separator + 1 < identity.size();
}
std::string ProvisionedIdentityCredential(const std::string &received,
const std::string &current) {
// A successful re-login can acknowledge the ID without echoing its password.
// Retain the credential used to authenticate that same ID.
if (!IsReusableIdentity(received) && IsReusableIdentity(current) &&
current.substr(0, current.find('@')) == received) return current;
return received;
}
NSString *IdentityCredentialAccount(NSString *host, NSInteger port) {
// Preserve the old server-scoped key so upgrades retain the same identity.
return [NSString stringWithFormat:@"CrossDeskIdentity.%@.%ld", host, (long)port];
}
NSMutableDictionary *IdentityCredentialQuery(NSString *account) {
return [@{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrService: @"cn.crossdesk.mobile.signaling-identity",
(__bridge id)kSecAttrAccount: account,
(__bridge id)kSecAttrSynchronizable: @NO,
} mutableCopy];
}
bool IsReusableIdentityValue(NSString *value) {
return [value isKindOfClass:NSString.class] && value.UTF8String &&
IsReusableIdentity(std::string(value.UTF8String));
}
OSStatus SaveIdentityCredential(NSString *account, NSString *value) {
if (!IsReusableIdentityValue(value)) return errSecParam;
NSMutableDictionary *query = IdentityCredentialQuery(account);
NSDictionary *attributes = @{
(__bridge id)kSecValueData: [value dataUsingEncoding:NSUTF8StringEncoding],
(__bridge id)kSecAttrAccessible:
(__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
};
// Update in place: deleting first could lose a valid credential on failure.
OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query,
(__bridge CFDictionaryRef)attributes);
if (status == errSecItemNotFound) {
NSMutableDictionary *item = [query mutableCopy];
[item addEntriesFromDictionary:attributes];
status = SecItemAdd((__bridge CFDictionaryRef)item, nullptr);
if (status == errSecDuplicateItem) {
status = SecItemUpdate((__bridge CFDictionaryRef)query,
(__bridge CFDictionaryRef)attributes);
}
}
if (status == errSecSuccess) {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:account];
}
return status;
}
OSStatus LoadIdentityCredential(NSString *account, NSString **value) {
*value = nil;
NSMutableDictionary *query = IdentityCredentialQuery(account);
query[(__bridge id)kSecReturnData] = @YES;
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
CFTypeRef result = nullptr;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
id object = CFBridgingRelease(result);
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
if (status == errSecSuccess) {
NSString *stored = [object isKindOfClass:NSData.class]
? [[NSString alloc] initWithData:object encoding:NSUTF8StringEncoding] : nil;
if (!IsReusableIdentityValue(stored)) return errSecDecode;
*value = stored;
// A Keychain value wins over a stale copy left by an interrupted migration.
[defaults removeObjectForKey:account];
return errSecSuccess;
}
if (status != errSecItemNotFound) return status;
NSString *legacy = [defaults objectForKey:account];
if (!legacy) return errSecItemNotFound;
if (!IsReusableIdentityValue(legacy)) {
// Older passwordless IDs cannot authenticate on a subsequent app launch.
[defaults removeObjectForKey:account];
return errSecItemNotFound;
}
status = SaveIdentityCredential(account, legacy);
if (status == errSecSuccess) *value = legacy;
return status;
}
OSStatus MigrateLegacyIdentityCredentials() {
OSStatus first_error = errSecSuccess;
for (NSString *key in NSUserDefaults.standardUserDefaults.dictionaryRepresentation) {
if (![key hasPrefix:@"CrossDeskIdentity."]) continue;
NSString *unused = nil;
OSStatus status = LoadIdentityCredential(key, &unused);
if (status != errSecSuccess && status != errSecItemNotFound &&
first_error == errSecSuccess) first_error = status;
}
return first_error;
}
OSStatus RemoveIdentityCredential(NSString *account) {
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)IdentityCredentialQuery(account));
if (status == errSecSuccess || status == errSecItemNotFound) {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:account];
return errSecSuccess;
}
return status;
}
void CopyCString(char *destination, size_t capacity,
const std::string &source) {
if (!destination || capacity == 0) return;
@@ -230,6 +338,7 @@ void DispatchMain(dispatch_block_t block) {
- (void)createIdentityPeer;
- (void)createControllerPeer;
- (void)destroyIdentityPeer;
- (void)reportIdentityStorageStatus:(OSStatus)status;
- (void)destroyControllerPeer;
@end
@@ -455,7 +564,16 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
const bool unchanged = self->_state->signal_host == host_value &&
self->_state->signal_port == signalPort &&
self->_state->identity_peer != nullptr;
if (unchanged) return;
const OSStatus migration_status = MigrateLegacyIdentityCredentials();
[self reportIdentityStorageStatus:migration_status];
if (unchanged) {
if (IsReusableIdentity(self->_state->identity_with_password)) {
NSString *value = [NSString stringWithUTF8String:self->_state->identity_with_password.c_str()];
[self reportIdentityStorageStatus:SaveIdentityCredential(
IdentityCredentialAccount(trimmed, signalPort), value)];
}
return;
}
[self destroyControllerPeer];
[self destroyIdentityPeer];
@@ -467,29 +585,26 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
self->_state->identity_with_password.clear();
self->_state->identity_base.clear();
NSString *key = [NSString stringWithFormat:@"CrossDeskIdentity.%@.%ld",
trimmed, (long)signalPort];
NSString *cached = [[NSUserDefaults standardUserDefaults]
stringForKey:key];
if (cached.length > 0) {
const std::string cached_identity = cached.UTF8String ?: "";
if (IsReusableIdentity(cached_identity)) {
self->_state->identity_with_password = cached_identity;
self->_state->identity_base = BaseIdentity(cached_identity);
DispatchMain(^{
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
if ([delegate respondsToSelector:
@selector(rtcBridge:didProvisionIdentity:)]) {
[delegate rtcBridge:self didProvisionIdentity:cached];
}
});
} else {
// A bare ID cannot authenticate after its first signaling session.
// Older iOS builds stored it anyway, causing every later launch to be
// rejected as "Incorrect password". Drop it and request a fresh
// server-issued ID/password pair.
[[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
}
NSString *cached = nil;
OSStatus load_status = LoadIdentityCredential(
IdentityCredentialAccount(trimmed, signalPort), &cached);
if (load_status == errSecSuccess) {
self->_state->identity_with_password = cached.UTF8String;
self->_state->identity_base = BaseIdentity(self->_state->identity_with_password);
NSString *identifier = [NSString stringWithUTF8String:self->_state->identity_base.c_str()];
DispatchMain(^{
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
if ([delegate respondsToSelector:@selector(rtcBridge:didProvisionIdentity:)]) {
[delegate rtcBridge:self didProvisionIdentity:identifier];
}
});
} else if (load_status != errSecItemNotFound) {
[self reportIdentityStorageStatus:load_status];
// Locked/unavailable Keychain is not the same as a missing identity.
[self handleSignalState:CrossDeskSignalStateCredentialUnavailable
role:PeerRole::Identity
generation:self->_identityGeneration.load()];
return;
}
[self createIdentityPeer];
});
@@ -842,6 +957,20 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
});
}
- (void)reportIdentityStorageStatus:(OSStatus)status {
const BOOL has_error = status != errSecSuccess;
if (has_error) {
// Log only the OSStatus, never an account, credential or Keychain payload.
NSLog(@"CrossDesk identity Keychain operation failed (%d)", (int)status);
}
DispatchMain(^{
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
if ([delegate respondsToSelector:@selector(rtcBridge:didChangeIdentityStorageError:)]) {
[delegate rtcBridge:self didChangeIdentityStorageError:has_error];
}
});
}
- (void)createIdentityPeer {
if (_state->identity_peer || _state->signal_host.empty()) return;
_state->identity_context.generation = _identityGeneration.fetch_add(1) + 1;
@@ -962,10 +1091,16 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
self->_state->identity_recovery_attempted = true;
NSString *host = [NSString stringWithUTF8String:
self->_state->signal_host.c_str()];
NSString *key = [NSString stringWithFormat:@"CrossDeskIdentity.%@.%d",
host,
self->_state->signal_port];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
OSStatus status = RemoveIdentityCredential(
IdentityCredentialAccount(host, self->_state->signal_port));
[self reportIdentityStorageStatus:status];
if (status != errSecSuccess) {
[self destroyIdentityPeer];
[self handleSignalState:CrossDeskSignalStateCredentialUnavailable
role:PeerRole::Identity
generation:self->_identityGeneration.load()];
return;
}
[self destroyIdentityPeer];
self->_state->identity_with_password.clear();
self->_state->identity_base.clear();
@@ -1078,25 +1213,25 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
const std::string identity_copy = identity;
dispatch_async(_rtcQueue, ^{
if (generation != self->_identityGeneration.load()) return;
self->_state->identity_with_password = identity_copy;
self->_state->identity_base = BaseIdentity(identity_copy);
const std::string credential = ProvisionedIdentityCredential(
identity_copy, self->_state->identity_with_password);
self->_state->identity_with_password = credential;
self->_state->identity_base = BaseIdentity(credential);
NSString *host = [NSString stringWithUTF8String:
self->_state->signal_host.c_str()];
NSString *key = [NSString stringWithFormat:@"CrossDeskIdentity.%@.%d",
host, self->_state->signal_port];
NSString *value = [NSString stringWithUTF8String:identity_copy.c_str()];
if (IsReusableIdentity(identity_copy)) {
[[NSUserDefaults standardUserDefaults] setObject:value forKey:key];
} else {
// Keep a passwordless identity only for this process. It is valid for
// the current login but is not safe to reuse on the next app launch.
[[NSUserDefaults standardUserDefaults] removeObjectForKey:key];
}
NSString *account = IdentityCredentialAccount(host, self->_state->signal_port);
NSString *value = [NSString stringWithUTF8String:credential.c_str()];
OSStatus status = IsReusableIdentity(credential)
? SaveIdentityCredential(account, value)
: RemoveIdentityCredential(account);
[self reportIdentityStorageStatus:status];
// Only the public device ID crosses into UI state; keep its password here.
NSString *identifier = [NSString stringWithUTF8String:self->_state->identity_base.c_str()];
DispatchMain(^{
if (generation != self->_identityGeneration.load()) return;
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
if ([delegate respondsToSelector:@selector(rtcBridge:didProvisionIdentity:)]) {
[delegate rtcBridge:self didProvisionIdentity:value];
[delegate rtcBridge:self didProvisionIdentity:identifier];
}
});
});
@@ -711,6 +711,10 @@ private struct ServerSettingsView: View {
} header: {
Text("服务器")
} footer: {
if let warning = session.identityStorageWarning {
Text(warning)
.foregroundStyle(.orange)
}
Text(session.localIdentity.isEmpty
? "正在获取本机 ID…"
: "本机 ID \(session.localIdentity)")
@@ -720,6 +724,20 @@ private struct ServerSettingsView: View {
.padding(.top, 16)
.textSelection(.enabled)
}
Section {
if let source = SourceMetadata.bundled {
Link(destination: source.privacyPolicyURL) {
Label("隐私政策", systemImage: "hand.raised")
}
}
NavigationLink {
ThirdPartyLicensesView()
} label: {
Label("关于与开源许可", systemImage: "doc.text")
}
} footer: {
Text("本应用使用 OpenFEC,遵循 CeCILL-C 许可。版权声明、许可全文和源码入口见“关于与开源许可”。")
}
}
.navigationTitle("设置")
.navigationBarTitleDisplayMode(.inline)
@@ -0,0 +1,39 @@
<?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>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
<string>3B52.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
<string>8FFB.1</string>
</array>
</dict>
</array>
</dict>
</plist>
@@ -332,6 +332,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
@Published private(set) var signalStatus = "正在连接信令服务"
@Published private(set) var connectionStatus = "未连接"
@Published private(set) var localIdentity = ""
@Published private(set) var identityStorageWarning: String?
@Published private(set) var isConnecting = false
@Published private(set) var isConnected = false
@Published private(set) var sessionVisible = false
@@ -828,6 +829,12 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
bridge.sendWindowsKeyCode(keyCode, isDown: isDown)
}
func rtcBridge(_ bridge: CrossDeskRTCBridge, didChangeIdentityStorageError hasError: Bool) {
identityStorageWarning = hasError
? "本机登录凭据暂时无法安全存取。请解锁设备后,在设置中点击“应用”重试。"
: nil
}
func rtcBridge(_ bridge: CrossDeskRTCBridge,
didChange state: CrossDeskSignalState) {
signalConnected = state.rawValue == 1
@@ -840,6 +847,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
case 4: signalStatus = "信令服务重连中"
case 5: signalStatus = "信令服务器已关闭连接"
case 6: signalStatus = "TLS 证书校验失败"
case 7: signalStatus = "无法访问本机登录凭据,请解锁后重试"
default: signalStatus = "未知信令状态"
}
if signalConnected {
File diff suppressed because one or more lines are too long
@@ -0,0 +1,185 @@
import SwiftUI
import UIKit
private struct LicenseDocument: Decodable, Identifiable {
let title: String
let text: String
var id: String { title }
}
private struct LicensedComponent: Decodable, Identifiable {
let id: String
let name: String
let version: String
let license: String
let sourceURL: URL
let buildSourceURL: URL?
let documents: [LicenseDocument]
var displayVersion: String {
if id == "crossdesk" {
return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
}
return version.count == 40 ? String(version.prefix(12)) : version
}
}
private struct LicenseCatalog: Decodable {
let schemaVersion: Int
let components: [LicensedComponent]
static let bundled: Result<LicenseCatalog, Error> = Result {
guard let url = Bundle.main.url(forResource: "ThirdPartyLicenses", withExtension: "json") else {
throw CocoaError(.fileNoSuchFile)
}
let catalog = try JSONDecoder().decode(LicenseCatalog.self, from: Data(contentsOf: url))
guard catalog.schemaVersion == 1, catalog.components.contains(where: { $0.id == "openfec" }) else {
throw CocoaError(.fileReadCorruptFile)
}
return catalog
}
}
struct SourceMetadata: Decodable {
let revision: String
let tag: String?
let isModified: Bool
let sourceURL: URL
let buildInstructionsURL: URL
let privacyPolicyURL: URL
static let bundled: SourceMetadata? = {
guard let url = Bundle.main.url(forResource: "SourceMetadata", withExtension: "json"),
let data = try? Data(contentsOf: url) else { return nil }
return try? JSONDecoder().decode(SourceMetadata.self, from: data)
}()
}
struct ThirdPartyLicensesView: View {
var body: some View {
Group {
switch LicenseCatalog.bundled {
case .success(let catalog):
List {
Section("本版本源码与构建说明") {
if let source = SourceMetadata.bundled {
Text(source.tag ?? String(source.revision.prefix(12)))
.textSelection(.enabled)
if source.isModified {
Text("开发版本包含尚未发布的修改,以下链接为基础版本源码。")
.font(.footnote)
.foregroundStyle(.secondary)
}
Link("CrossDesk 源码", destination: source.sourceURL)
Link("获取源码、修改与重新构建", destination: source.buildInstructionsURL)
Link("隐私政策", destination: source.privacyPolicyURL)
}
Text("各组件的使用、复制、修改和再分发权利以相应开源许可证为准。普通使用条款不限制这些许可证授予的权利。")
.font(.footnote)
.foregroundStyle(.secondary)
}
Section("OpenFEC 告知") {
Text("本应用使用 OpenFEC \(catalog.components.first(where: { $0.id == "openfec" })?.displayVersion ?? "") 提供前向纠错功能,遵循 CeCILL-C 1.0 许可。")
Text("(c) Copyright 2009 - 2012 INRIA - All rights reserved")
.font(.footnote)
Text("OpenFEC 按其许可提供有限保证,作者、权利人和后续许可人的责任亦受限制。完整声明、许可条款及源码见下方。")
.font(.footnote)
.foregroundStyle(.secondary)
if let openFEC = catalog.components.first(where: { $0.id == "openfec" }) {
NavigationLink("OpenFEC 许可与源码") {
ComponentLicenseView(component: openFEC)
}
}
}
Section {
ForEach(catalog.components) { component in
NavigationLink {
ComponentLicenseView(component: component)
} label: {
VStack(alignment: .leading, spacing: 4) {
Text(component.name)
Text(component.displayVersion + " · " + component.license)
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
} header: {
Text("开源组件")
} footer: {
Text("许可全文和版权声明可离线阅读;查看源码需要网络连接。")
}
}
case .failure:
VStack(spacing: 12) {
Image(systemName: "doc.text.magnifyingglass")
.font(.largeTitle)
Text("无法读取开源许可")
.font(.headline)
Text("应用中的许可文件缺失或损坏,请重新安装应用。")
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.padding()
}
}
.navigationTitle("关于与开源许可")
.navigationBarTitleDisplayMode(.inline)
}
}
private struct ComponentLicenseView: View {
let component: LicensedComponent
var body: some View {
List {
Section {
Text(component.name).font(.headline)
Text(component.displayVersion).textSelection(.enabled)
Text(component.license).foregroundStyle(.secondary)
Link("查看源码", destination: component.id == "crossdesk"
? SourceMetadata.bundled?.sourceURL ?? component.sourceURL
: component.sourceURL)
if let buildSourceURL = component.buildSourceURL {
Link("构建配方与项目补丁", destination: buildSourceURL)
}
}
Section("许可与版权声明") {
ForEach(component.documents) { document in
NavigationLink {
LicenseTextView(text: document.text)
.navigationTitle(document.title)
.navigationBarTitleDisplayMode(.inline)
} label: {
Text(document.title == "Source copyright and license notices"
? "源码版权与许可声明" : document.title)
}
}
}
}
.navigationTitle(component.name)
.navigationBarTitleDisplayMode(.inline)
}
}
// UITextView keeps long license documents selectable and efficient to scroll.
private struct LicenseTextView: UIViewRepresentable {
let text: String
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.isEditable = false
view.isSelectable = true
view.dataDetectorTypes = [.link]
view.adjustsFontForContentSizeCategory = true
view.font = .preferredFont(forTextStyle: .body)
view.textColor = .label
view.backgroundColor = .systemBackground
view.textContainerInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
return view
}
func updateUIView(_ view: UITextView, context: Context) {
if view.text != text { view.text = text }
}
}
+16
View File
@@ -0,0 +1,16 @@
<?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>method</key>
<string>app-store-connect</string>
<key>destination</key>
<string>export</string>
<key>signingStyle</key>
<string>automatic</string>
<key>manageAppVersionAndBuildNumber</key>
<false/>
<key>uploadSymbols</key>
<true/>
</dict>
</plist>
+14
View File
@@ -0,0 +1,14 @@
<?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>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array/>
</dict>
</plist>
+174 -2
View File
@@ -7,7 +7,7 @@ There is no `WKWebView` or browser runtime.
## Requirements
- Xcode 16 or newer
- Xmake available on `PATH`
- Xmake 3.1.1 available on `PATH` (or selected with `XMAKE_BIN`)
- A physical arm64 iPhone or iPad running iOS 16 or newer
## Build
@@ -22,6 +22,28 @@ cd crossdesk
For an existing checkout, run `git submodule update --init --recursive` from
the repository root. All remaining commands below use that root directory.
For a released app, select the tag shown in **Settings → About and open-source
licenses** when cloning: `git clone --branch <source-tag> --recurse-submodules
https://github.com/kunkundi/crossdesk.git`. The iOS marketing version is independent
of the repository tag; use the source tag or commit recorded in the app.
Install [Xmake 3.1.1](https://github.com/xmake-io/xmake/releases/tag/v3.1.1),
CMake, and a current Python 3 with setuptools/wheel. Pin the package repository
in a separate Xmake global directory before building:
```sh
export XMAKE_GLOBALDIR="$HOME/.cache/crossdesk/xmake-ios"
xmake repo --update
git -C "$XMAKE_GLOBALDIR/.xmake/repositories/xmake-repo" fetch origin 121109352da272cd1a3d4ed52f2dcda34f839955
git -C "$XMAKE_GLOBALDIR/.xmake/repositories/xmake-repo" checkout --detach 121109352da272cd1a3d4ed52f2dcda34f839955
```
Keep `XMAKE_GLOBALDIR` set for command-line Xcode builds. For builds launched
from the Xcode app, set it as a user-defined build setting on the target, or
pin the default `~/.xmake/repositories/xmake-repo` instead. The build checks
the tool version, package repository commit and reviewed recipe hashes.
Application dependencies are built from source, with Xmake precompiled
package downloads disabled.
1. Open `apps/ios/CrossDeskMobile.xcodeproj` and select the `CrossDeskMobile`
target and scheme.
@@ -34,7 +56,8 @@ the repository root. All remaining commands below use that root directory.
The first build phase builds MiniRTC and the CrossDesk wire library, downloads
and builds their dependencies, then merges the iPhoneOS archives into a local library under
`Vendor/`. This build path targets physical devices; simulator builds are
`Vendor/`. OpenSSL is kept separate and packaged as a static framework with SDK
privacy resources in the Xcode products directory. This build path targets physical devices; simulator builds are
not supported. The exact tool versions used by CI are recorded in the
[workflow](../../.github/workflows/build.yml).
@@ -67,6 +90,74 @@ decoding is currently unsupported. Desktop builds retain OpenH264.
The native build script checks the merged archive for OpenH264 entry points
and rejects a build if they are present, including in cached archives.
## Third-party licenses
**设置 → 关于与开源许可** provides offline license texts, copyright notices,
and source links. OpenFEC is acknowledged both on the settings page and at the
top of the notices page, with its INRIA copyright, CeCILL-C terms and limited
warranty/liability notice. Its embedded BSD and CC-BY-SA notices are preserved
in the source-notices document as well.
`licenses/sources.json` pins the reviewed source archives (SHA-256), Git
revisions, selected legal files and source-notice extraction rules.
`CrossDeskMobile/Resources/ThirdPartyLicenses.json` is the generated resource
bundled by Xcode. The catalog covers 34 components for the default iOS build,
including header-only dependencies, GLib's proxy-libintl, spdlog's bundled fmt,
WebRTC-derived code, inih, and the applicable patent notices. Host build tools,
OpenH264, desktop-only libraries, and the disabled libpsl built-in suffix list
are excluded.
Regenerate from the pinned source distributions with Python 3.9 or newer:
```sh
python3 apps/ios/scripts/generate_third_party_licenses.py --generate
```
The collector uses original checksum-verified archives, including GLib's
`COPYING` file even when Xmake excludes it from an extracted build directory.
It reuses matching Xmake downloads or caches downloads outside the repository
in `~/.cache/crossdesk/licenses/`. License text is preserved, with explicit
character decoding for the legacy OpenFEC files. Copyright comment blocks are
collected from the reviewed runtime source directories; the catalog is an
attribution superset, not a claim that every source file survives linker
dead stripping.
Normal Xcode builds verify the checked-in resource offline, compare all resolved
dependency versions (including header-only packages) with the catalog, check
the merged archive's input hashes and local package recipes, and check the
MiniRTC revision. A missing, stale or altered catalog fails the build. When
dependencies or MiniRTC change, review the corresponding source notices and
update the lock before regenerating; regeneration alone does not select a new
dependency version. `MINIRTC_ENABLE_AOM=true` also requires a reviewed AOM entry.
The source screen links to the exact application tag/commit, MiniRTC commit,
and custom dependency recipes/patches. `SourceMetadata.json` is generated in
the app bundle, not in the checkout. Development builds with local edits are
explicitly identified as modified. Version-tag CI builds reject an unclean
checkout or a mismatched tag. The release archive also carries
`ThirdPartySources.json`, `SourceMetadata.json` and the open-source rights
notice; release notes provide the corresponding source and build links.
Keep the source tags, submodule commits and referenced dependency source
archives available for recipients of that release.
To change a library, edit MiniRTC in its submodule, or modify the relevant
package recipe/patch under `deps/submodules/minirtc/thirdparty`. If a package
recipe changes, change its package configuration revision (or rebuild that
package) so Xmake does not reuse a previous binary. Review affected notices,
update recipe hashes, commit MiniRTC, update its revision in `sources.json`,
then regenerate the catalog. Build the application again to relink it with
the modified library. Use your own team and bundle identifier to install the
modified build; the maintainer's signing credentials are not required.
These materials implement source identification and attribution. They do not
by themselves resolve GPL/LGPL distribution-term compatibility or grant
additional permissions from other copyright holders. Applicable modification,
reverse-engineering and installation-information rights must also be respected.
See [LGPLv3 section 4](https://www.gnu.org/licenses/lgpl-3.0.html) and
[GPLv3 section 6](https://www.gnu.org/licenses/gpl-3.0.html).
## Optional codecs
The unused libaom backends are excluded by default. To include them for development,
set the `MINIRTC_ENABLE_AOM` environment variable (or Xcode user-defined build setting)
to `true`. This includes libaom in the merged native archive without changing the
@@ -106,3 +197,84 @@ If decoded frames do not reach the bridge, check that the selected stream is
named `Display1`, `Display2`, and so on. If no frames decode, check the selected
codec and the desktop capture/encoding logs, then reconnect or switch displays
to request a new key frame.
## App Store archive and bundle validation
OpenSSL is on Apple's
[required SDK privacy-manifest list](https://developer.apple.com/support/third-party-SDK-requirements/).
The iOS build compiles OpenSSL from source, packages it separately as
`OpenSSL.framework`, then links and embeds it using Xcode's
[static framework support](https://developer.apple.com/documentation/xcode/creating-a-static-framework).
Xcode removes the static archive from the embedded framework (it may insert a
small resource-framework stub) and preserves `PrivacyInfo.xcprivacy`, source
metadata and license texts. The application does not load a dynamic OpenSSL copy.
The iOS-specific OpenSSL recipe disables POSIX metadata calls, automatic
`openssl.cnf` loading, and the unused file-URI store provider. It selects the
upstream `getrandom` entropy backend, which uses Darwin's `getentropy` on the
supported iOS versions; the random-device fallback is not compiled. Existing
in-memory certificate/key handling remains available, while `OSSL_STORE` file
URLs are intentionally unsupported. Signaling trust and hostname validation
use Apple Security rather than OpenSSL's build-host certificate paths.
Desktop OpenSSL builds are unchanged.
`package_openssl_ios.py` checks the actual OpenSSL archives for file metadata,
disk-space and boot-time API imports before producing the framework. Its
`sysctlbyname` calls are limited to the reviewed `armcap.c` CPU implementation
selection, not fingerprinting. The SDK privacy manifest declares no tracking,
SDK data collection or required-reason API use for this specific configuration.
This is not a declaration about generic OpenSSL builds or the application's
signaling/relay data processing. Source or configuration changes require another
review; the packager rejects known forbidden imports even when reusing a cache.
`PrivacyInfo.xcprivacy` declares app preferences (`CA92.1`), sandbox and
user-selected file metadata (`C617.1`, `3B52.1`), and elapsed/event timing
(`35F9.1`, `8FFB.1`). MiniRTC uses timing for pacing, timeouts, RTT and media
packet timestamps. The iOS GLib recipe disables unused filesystem-capacity
and system-volume discovery. These APIs must not be repurposed for fingerprinting.
The app-level manifest does not assert that the service collects no data: App Store
Connect privacy answers also depend on actual signaling/relay-server practices.
The application links to the [privacy policy](../../PRIVACY.md), including iOS
clipboard, file, local thumbnail and Keychain behavior.
Validate a built bundle using:
```sh
python3 apps/ios/scripts/verify_app_bundle.py /path/to/CrossDeskMobile.app
```
For a release, first commit all application and submodule changes, regenerate
notices for the committed dependency revision, and check out the intended
version tag. Publish the tag and its referenced MiniRTC commit to the public repositories
before distributing the app. Local tag verification cannot prove that recipients
can fetch an unpublished commit. With an App Store-capable development team
configured in Xcode:
```sh
xcodebuild -project apps/ios/CrossDeskMobile.xcodeproj \
-scheme CrossDeskMobile -configuration Release \
-destination 'generic/platform=iOS' \
-archivePath /tmp/CrossDeskMobile.xcarchive \
CROSSDESK_RELEASE_BUILD=YES archive
python3 apps/ios/scripts/verify_app_bundle.py \
/tmp/CrossDeskMobile.xcarchive/Products/Applications/CrossDeskMobile.app \
--require-release
xcodebuild -exportArchive -archivePath /tmp/CrossDeskMobile.xcarchive \
-exportOptionsPlist apps/ios/ExportOptions-AppStore.plist \
-exportPath /tmp/CrossDeskMobile-AppStore
```
Set a new `CURRENT_PROJECT_VERSION` for each upload. The export configuration
creates a local App Store export; it does not submit the app. Distribution
signing requires the appropriate Apple Developer Program membership and
provisioning. The unsigned GitHub artifact is for development, not an App Store
submission. Organizer/App Store Connect validation remains necessary.
The app embeds OpenSSL and libsrtp. Do not declare that encryption is limited
to Apple system APIs or set `ITSAppUsesNonExemptEncryption=NO` without completing
the applicable [export-compliance determination](https://developer.apple.com/help/app-store-connect/manage-app-information/overview-of-export-compliance).
After that determination, supply the appropriate declaration and any required
approval code in the distribution build. The repository deliberately does not
preselect an exemption.
+24
View File
@@ -0,0 +1,24 @@
CrossDesk 开源软件权利说明 / Open-source software rights
CrossDesk 按 GNU GPL 第 3 版发布,MiniRTC 按 GNU LGPL 第 3 版发布。
其他组件适用各自列明的许可证。使用、复制、修改及再分发这些组件的权利,
以相应许可证为准;本应用的普通使用条款不限制开源许可证授予你的权利。
许可证中的保证、责任限制及其他条件仍然适用。
“本版本源码与构建说明”提供与发行版本对应的源码入口、子模块信息、
依赖配方、补丁及构建说明。可以修改相关库后重新构建和链接应用。
在自己的 iPhone 或 iPad 上运行修改版时,需要使用自己的 Apple 开发签名,
并按构建文档设置开发团队和 Bundle Identifier。
CrossDesk is distributed under GNU GPL version 3; MiniRTC is distributed
under GNU LGPL version 3. Other components are governed by their listed
licenses. Your rights to use, copy, modify and redistribute those components
are governed by the applicable licenses. The application's ordinary terms
of use do not restrict rights granted by those open-source licenses.
The licenses' warranty, liability and other provisions continue to apply.
The version-specific source and build links identify the application source,
submodules, dependency recipes, patches and build instructions. You can
modify the libraries and rebuild and relink the application. Running a
modified build on your own iPhone or iPad requires your own Apple development
signing setup, team and bundle identifier as described in the build guide.
+906
View File
@@ -0,0 +1,906 @@
{
"schema_version": 1,
"platform": "iphoneos-arm64",
"components": [
{
"id": "asio",
"name": "Asio",
"version": "1.32.0",
"license": "BSL-1.0",
"source": {
"kind": "archive",
"url": "https://github.com/chriskohlhoff/asio/archive/refs/tags/asio-1-32-0.tar.gz",
"sha256": "f1b94b80eeb00bb63a3c8cef5047d4e409df4d8a3fe502305976965827d95672"
},
"documents": [
{
"path": "asio/COPYING",
"sha256": "5cc69ca9afd3417ed8e58bcd9c0bb730375e9e3e92b5aaf6483b93a2837e0cac"
},
{
"path": "asio/LICENSE_1_0.txt",
"sha256": "c9bff75738922193e67fa726fa225535870d2aa1059f91452c411736284ad566"
}
],
"recipe_sha256": "52bddc12cf0837117e935ea5d71aa5c35e7fd6ae802aa7401c6dba598cae0989",
"recipe_path": "xmake-repo/packages/a/asio/xmake.lua",
"package": "asio",
"notice_prefixes": [
"asio/include/"
]
},
{
"id": "concurrentqueue",
"name": "ConcurrentQueue",
"version": "v1.0.4",
"license": "(BSD-2-Clause OR BSL-1.0) / Zlib (semaphore)",
"source": {
"kind": "archive",
"url": "https://github.com/cameron314/concurrentqueue/archive/refs/tags/v1.0.4.tar.gz",
"sha256": "87fbc9884d60d0d4bf3462c18f4c0ee0a9311d0519341cac7cbd361c885e5281"
},
"documents": [
{
"path": "LICENSE.md",
"sha256": "22331571e5e3dbe4c79ad715e23127bdf3beabd75052c953ddabf191ef799e14"
}
],
"recipe_sha256": "23c69dc09488527b0d6adf1d25f795a9f0afdc7a1d594c44603c2dd07162faa4",
"recipe_path": "xmake-repo/packages/c/concurrentqueue/xmake.lua",
"package": "concurrentqueue",
"notice_prefixes": [
"concurrentqueue.h",
"blockingconcurrentqueue.h",
"lightweightsemaphore.h"
]
},
{
"id": "crossdesk",
"name": "CrossDesk / crossdesk_wire",
"version": "application source",
"license": "GPL-3.0-only",
"source": {
"kind": "repository",
"url": "https://github.com/kunkundi/crossdesk",
"revision": null
},
"documents": [
{
"path": "LICENSE",
"sha256": "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986"
},
{
"path": "apps/ios/licenses/OPEN_SOURCE_NOTICE.txt",
"sha256": "34720d126e6373603d0c1802a8c3276802b93f2a8fbbf2b61837c6fdefea8877"
}
]
},
{
"id": "dav1d",
"name": "dav1d",
"version": "1.4.3",
"license": "BSD-2-Clause and patent grant",
"source": {
"kind": "archive",
"url": "https://code.videolan.org/videolan/dav1d/-/archive/e9986de7f4264940af6fa1df1498fd2754077de4/dav1d-e9986de7f4264940af6fa1df1498fd2754077de4.tar.gz",
"sha256": "ba147ff83d6c73f578a12f8eece636226e9c1f40cd55a11841a3bcb2c88cd46a",
"revision": "e9986de7f4264940af6fa1df1498fd2754077de4"
},
"documents": [
{
"path": "COPYING",
"sha256": "b327887de263238deaa80c34cdd2ff3e0ba1d35db585ce14a37ce3e74ee389e9"
},
{
"path": "doc/PATENTS",
"sha256": "335eca574598bf4ca181b12f708d6669e5a5e78c8e1513e5b35fa1f03901484b"
}
],
"package": "dav1d",
"notice_prefixes": [
"src/",
"include/"
]
},
{
"id": "glib",
"name": "GLib / GObject / GIO / GModule / GThread / GVDB",
"version": "2.84.1",
"license": "LGPL-2.1-or-later and embedded notices",
"source": {
"kind": "archive",
"url": "https://download.gnome.org/sources/glib/2.84/glib-2.84.1.tar.xz",
"sha256": "2b4bc2ec49611a5fc35f86aca855f2ed0196e69e53092bab6bb73396bf30789a"
},
"documents": [
{
"path": "COPYING",
"sha256": "fa6f36630bb1e0c571d34b2bbdf188d08495c9dbf58f28cac112f303fc1f58fb"
},
{
"path": "LICENSES/Apache-2.0.txt",
"sha256": "074e6e32c86a4c0ef8b3ed25b721ca23aca83df277cd88106ef7177c354615ff"
},
{
"path": "LICENSES/CC0-1.0.txt",
"sha256": "a2010f343487d3f7618affe54f789f5487602331c0a8d03f49e9a7c547cf0499"
},
{
"path": "LICENSES/LGPL-2.1-only.txt",
"sha256": "fa6f36630bb1e0c571d34b2bbdf188d08495c9dbf58f28cac112f303fc1f58fb"
},
{
"path": "LICENSES/LLVM-exception.txt",
"sha256": "e34c58338bd89d43e709e226610d8f32b3e3c47f4ad9a99a8dc1d4ac7842488e"
},
{
"path": "LICENSES/MIT.txt",
"sha256": "b85dcd3e453d05982552c52b5fc9e0bdd6d23c6f8e844b984a88af32570b0cc0"
},
{
"path": "LICENSES/MPL-1.1.txt",
"sha256": "6214f8b1300bb9f37b16ebc146f8f61af0187e0025042ff95c8b3030744a8795"
},
{
"path": "subprojects/gvdb/COPYING",
"sha256": "bdbcdaa7858872393e9d6b216bbdc8a23e99d98dd6cdb7cda19458025b9e19fc"
}
],
"recipe_sha256": "773cac2e1a5aec087abc9347c7278f3fca40e36212b33a5f01012875cfbf7e6c",
"recipe_path": "deps/submodules/minirtc/thirdparty/glib/xmake.lua",
"package": "glib",
"notice_prefixes": [
"glib/",
"gobject/",
"gio/",
"gmodule/",
"subprojects/gvdb/"
]
},
{
"id": "gssdp",
"name": "GSSDP",
"version": "1.6.4",
"license": "LGPL-2.1-or-later",
"source": {
"kind": "archive",
"url": "https://download.gnome.org/sources/gssdp/1.6/gssdp-1.6.4.tar.xz",
"sha256": "ff97fdfb7f561d3e6813b4f6a2145259e7c2eff43cc0e63f3fd031d0b6266032"
},
"documents": [
{
"path": "COPYING",
"sha256": "dc626520dcd53a22f727af3ee42c770e56c97a64fe3adb063799d8ab032fe551"
},
{
"path": "AUTHORS",
"sha256": "66b5509fcdd21acaab5d7a3856e699bada9e9ebf0485312abbe987711bc7bd27"
}
],
"recipe_sha256": "52667099a445f7004b9200df4ed771dd74e73745a5c6dc7fe728168e9f8b3e7a",
"recipe_path": "deps/submodules/minirtc/thirdparty/gupnp/xmake.lua",
"package": "gssdp",
"notice_prefixes": [
"libgssdp/"
]
},
{
"id": "gupnp",
"name": "GUPnP",
"version": "1.6.9",
"license": "LGPL-2.1-or-later",
"source": {
"kind": "archive",
"url": "https://download.gnome.org/sources/gupnp/1.6/gupnp-1.6.9.tar.xz",
"sha256": "2edb6ee3613558e62f538735368aee27151b7e09d4e2e2c51606833da801869b"
},
"documents": [
{
"path": "COPYING",
"sha256": "dc626520dcd53a22f727af3ee42c770e56c97a64fe3adb063799d8ab032fe551"
},
{
"path": "AUTHORS",
"sha256": "fadd2ab40439bc89d2e9540c02aef1f23edea96983af9cceaef345a9423d3c70"
}
],
"recipe_sha256": "52667099a445f7004b9200df4ed771dd74e73745a5c6dc7fe728168e9f8b3e7a",
"recipe_path": "deps/submodules/minirtc/thirdparty/gupnp/xmake.lua",
"package": "gupnp",
"notice_prefixes": [
"libgupnp/"
]
},
{
"id": "gupnp-igd",
"name": "GUPnP IGD",
"version": "1.6.0",
"license": "LGPL-2.1-or-later",
"source": {
"kind": "archive",
"url": "https://download.gnome.org/sources/gupnp-igd/1.6/gupnp-igd-1.6.0.tar.xz",
"sha256": "4099978339ab22126d4968f2a332b6d094fc44c78797860781f1fc2f11771b74"
},
"documents": [
{
"path": "COPYING",
"sha256": "6095e9ffa777dd22839f7801aa845b31c9ed07f3d6bf8a26dc5d2dec8ccc0ef3"
},
{
"path": "AUTHORS",
"sha256": "77a943ea347578cfcffc032616707d43e5f5e03a12f864c0d421c3d632fc1582"
}
],
"recipe_sha256": "52667099a445f7004b9200df4ed771dd74e73745a5c6dc7fe728168e9f8b3e7a",
"recipe_path": "deps/submodules/minirtc/thirdparty/gupnp/xmake.lua",
"package": "gupnp-igd",
"notice_prefixes": [
"libgupnp-igd/"
]
},
{
"id": "inih",
"name": "inih / INIReader",
"version": "6286ae6c1c015609d3070cc139e5feea900caf7c",
"license": "BSD-3-Clause",
"source": {
"kind": "repository",
"url": "https://github.com/kunkundi/minirtc",
"revision": "6286ae6c1c015609d3070cc139e5feea900caf7c"
},
"documents": [
{
"path": "deps/submodules/minirtc/src/inih/LICENSE",
"sha256": "bd2a675a5a26fbff77f171f3a15afb1a9c7855be1a01c7e3fd16b8d2617acdda"
}
]
},
{
"id": "nlohmann_json",
"name": "JSON for Modern C++ / Hedley",
"version": "v3.11.3",
"license": "MIT",
"source": {
"kind": "archive",
"url": "https://github.com/nlohmann/json/archive/refs/tags/v3.11.3.tar.gz",
"sha256": "0d8ef5af7f9794e3263480193c491549b2ba6cc74bb018906202ada498a79406"
},
"documents": [
{
"path": "LICENSE.MIT",
"sha256": "86b998c792894ccb911a1cb7994f7a9652894e7a094c0b5e45be2f553f45cf14"
}
],
"recipe_sha256": "eadd211f0521a18207e9544c0c5a08c41dca578fce99df5c2577f6fcf2a0fdce",
"recipe_path": "xmake-repo/packages/n/nlohmann_json/xmake.lua",
"package": "nlohmann_json",
"notice_prefixes": [
"include/"
]
},
{
"id": "kcp",
"name": "KCP",
"version": "1.7",
"license": "MIT",
"source": {
"kind": "archive",
"url": "https://github.com/skywind3000/kcp/archive/refs/tags/1.7.tar.gz",
"sha256": "b4d26994d95599ab0c44e1f93002f9fda275094a879d66c192d79d596529199e"
},
"documents": [
{
"path": "LICENSE",
"sha256": "6587fd02a924b97dbd6758b0573ddec056fb494a84f3709c5aaa33ce1f7f029c"
}
],
"recipe_sha256": "f86cac2c4b884f3347a39a59f3dd8de566c257c7fd481c6e49b05f4730f12c4f",
"recipe_path": "xmake-repo/packages/k/kcp/xmake.lua",
"package": "kcp",
"notice_prefixes": [
"ikcp.c",
"ikcp.h"
]
},
{
"id": "libdatachannel",
"name": "libdatachannel",
"version": "v0.24.5",
"license": "MPL-2.0",
"source": {
"kind": "archive",
"url": "https://github.com/paullouisageneau/libdatachannel/archive/refs/tags/v0.24.5.tar.gz",
"sha256": "454537c3cd526bed935d847bb2dff4046f266eef84d43b2a5f2f2f293c0026f4"
},
"documents": [
{
"path": "LICENSE",
"sha256": "fab3dd6bdab226f1c08630b1dd917e11fcb4ec5e1e020e2c16f83a0a13863e85"
}
],
"recipe_sha256": "b3ec15652a47d65e0a1f85f62d02c098c5cf5b8d8c3cf7b03ff099efa7016326",
"recipe_path": "deps/submodules/minirtc/thirdparty/libdatachannel/xmake.lua",
"package": "libdatachannel",
"notice_prefixes": [
"include/",
"src/"
]
},
{
"id": "libffi",
"name": "libffi",
"version": "3.4.8",
"license": "MIT",
"source": {
"kind": "archive",
"url": "https://github.com/libffi/libffi/releases/download/v3.4.8/libffi-3.4.8.tar.gz",
"sha256": "bc9842a18898bfacb0ed1252c4febcc7e78fa139fd27fdc7a3e30d9d9356119b"
},
"documents": [
{
"path": "LICENSE",
"sha256": "67894089811f93fca47a76f85e017da6f8582d4ba0905963c6e0f1ad6df7a195"
}
],
"recipe_sha256": "de19ac4e3e91ef5715ac582c75e0d10404dc14ec4e33b2e764dc2637a78d24e9",
"recipe_path": "xmake-repo/packages/l/libffi/xmake.lua",
"package": "libffi",
"notice_prefixes": [
"src/",
"include/"
]
},
{
"id": "libnice",
"name": "libnice",
"version": "0.1.24",
"license": "MPL-1.1 OR LGPL-2.1-or-later",
"source": {
"kind": "archive",
"url": "https://gitlab.freedesktop.org/libnice/libnice/-/archive/0.1.24/libnice-0.1.24.tar.gz",
"sha256": "1da5ac13ed5d4e175e0d2d46ad3748c6635244f8f3eb2b8e31578ef59aa2ddce"
},
"documents": [
{
"path": "COPYING",
"sha256": "e5504e5ffe894870ea1a652df957a120ead9c3f9a5e9d314ca41ba06f2d72024"
},
{
"path": "COPYING.LGPL",
"sha256": "a9bdde5616ecdd1e980b44f360600ee8783b1f99b8cc83a2beb163a0a390e861"
},
{
"path": "COPYING.MPL",
"sha256": "3b3c75ae6c663c4f93987647eb5463d33dc1602a1aded3ac479773f2c3928e3a"
},
{
"path": "AUTHORS",
"sha256": "d72eb6fa9d5e93abb11d13b6c6f04fda2ba183d0705e703887cda243f6f1eead"
}
],
"recipe_sha256": "7d7c30d7d63245d0190e64b050184f6ff6933ef9133bff5329d62a7b9b685441",
"recipe_path": "deps/submodules/minirtc/thirdparty/libnice/xmake.lua",
"package": "libnice",
"notice_prefixes": [
"agent/",
"nice/",
"random/",
"socket/",
"stun/"
]
},
{
"id": "upnp-libpsl",
"name": "libpsl",
"version": "0.21.5",
"license": "MIT / BSD-3-Clause",
"source": {
"kind": "archive",
"url": "https://github.com/rockdaboot/libpsl/releases/download/0.21.5/libpsl-0.21.5.tar.gz",
"sha256": "1dcc9ceae8b128f3c0b3f654decd0e1e891afc6ff81098f227ef260449dae208"
},
"documents": [
{
"path": "LICENSE",
"sha256": "322392e6001b2b3b0fca705dc9f11f26e12441188ecde50eb1d22909980b2925"
},
{
"path": "src/LICENSE.chromium",
"sha256": "1d7f52747a9169751cdf2641a8299c0098e9ecdf9429296ffd55bdb14c9ed5b3"
}
],
"recipe_sha256": "52667099a445f7004b9200df4ed771dd74e73745a5c6dc7fe728168e9f8b3e7a",
"recipe_path": "deps/submodules/minirtc/thirdparty/gupnp/xmake.lua",
"package": "upnp-libpsl",
"notice_prefixes": [
"src/",
"include/"
]
},
{
"id": "libsoup",
"name": "libsoup",
"version": "3.6.5",
"license": "LGPL-2.0-or-later",
"source": {
"kind": "archive",
"url": "https://download.gnome.org/sources/libsoup/3.6/libsoup-3.6.5.tar.xz",
"sha256": "6891765aac3e949017945c3eaebd8cc8216df772456dc9f460976fbdb7ada234"
},
"documents": [
{
"path": "COPYING",
"sha256": "b7993225104d90ddd8024fd838faf300bea5e83d91203eab98e29512acebd69c"
},
{
"path": "AUTHORS",
"sha256": "f8776285b4556368878a1b903a1f0e6d6cee342284961562a7716a2dacc28321"
}
],
"recipe_sha256": "52667099a445f7004b9200df4ed771dd74e73745a5c6dc7fe728168e9f8b3e7a",
"recipe_path": "deps/submodules/minirtc/thirdparty/gupnp/xmake.lua",
"package": "libsoup",
"notice_prefixes": [
"libsoup/"
]
},
{
"id": "libsrtp",
"name": "libsrtp",
"version": "v2.7.0",
"license": "BSD-3-Clause",
"source": {
"kind": "archive",
"url": "https://github.com/cisco/libsrtp/archive/refs/tags/v2.7.0.tar.gz",
"sha256": "54facb1727a557c2a76b91194dcb2d0a453aaf8e2d0cbbf1e3c2848c323e28ad"
},
"documents": [
{
"path": "LICENSE",
"sha256": "8e19d42a1eec9561f3f347253ddf2e385c55f392f025bb0fd41b88dbf38db5ae"
}
],
"recipe_sha256": "9629edb8d395ea3017af1702cafea57173ca0a6a1ea305a6174b1936598faec6",
"recipe_path": "deps/submodules/minirtc/thirdparty/libsrtp/xmake.lua",
"package": "libsrtp",
"notice_prefixes": [
"srtp/",
"crypto/",
"include/"
]
},
{
"id": "upnp-libxml2",
"name": "libxml2",
"version": "2.14.6",
"license": "MIT and embedded notices",
"source": {
"kind": "archive",
"url": "https://download.gnome.org/sources/libxml2/2.14/libxml2-2.14.6.tar.xz",
"sha256": "7ce458a0affeb83f0b55f1f4f9e0e55735dbfc1a9de124ee86fb4a66b597203a"
},
"documents": [
{
"path": "Copyright",
"sha256": "5d4873884a890122a4b9b20ad56ac6f7da1d796a5bfcf04a427970ac96217626"
}
],
"recipe_sha256": "52667099a445f7004b9200df4ed771dd74e73745a5c6dc7fe728168e9f8b3e7a",
"recipe_path": "deps/submodules/minirtc/thirdparty/gupnp/xmake.lua",
"package": "upnp-libxml2",
"notice_prefixes": [
"include/"
],
"notice_root_files": true
},
{
"id": "libyuv",
"name": "libyuv",
"version": "2025.8.14",
"license": "BSD-3-Clause and patent grant",
"source": {
"kind": "archive",
"url": "https://codeload.github.com/kunkundi/libyuv/tar.gz/ec10b61c58ee4b8fbe648d2744d9dad9ccba6430",
"sha256": "e6e83acefcb9322a498a04c2c417491ceab18cf9750d306110ab5ab27d84377b",
"revision": "ec10b61c58ee4b8fbe648d2744d9dad9ccba6430"
},
"documents": [
{
"path": "LICENSE",
"sha256": "2b2cc1180c7e6988328ad2033b04b80117419db9c4c584918bbb3cfec7e9364f"
},
{
"path": "PATENTS",
"sha256": "4da84211f947c6f9f7dd8fbce513e1522e01833407ae732b8c9a2bbb75fed495"
},
{
"path": "AUTHORS",
"sha256": "0c4a51c72a06b3431bb2b750e3b04dbe3694b22e2fcae6e370216424eec56d52"
}
],
"package": "libyuv",
"notice_prefixes": [
"source/",
"include/"
]
},
{
"id": "minirtc",
"name": "MiniRTC",
"version": "6286ae6c1c015609d3070cc139e5feea900caf7c",
"license": "LGPL-3.0-only",
"source": {
"kind": "repository",
"url": "https://github.com/kunkundi/minirtc",
"revision": "6286ae6c1c015609d3070cc139e5feea900caf7c"
},
"documents": [
{
"path": "deps/submodules/minirtc/LICENSE",
"sha256": "ea8af5e789cb2d4e9b10bce3874982ade163b749b6bfbdb32e2df21c4d106de1"
}
]
},
{
"id": "nghttp2",
"name": "nghttp2",
"version": "1.68.0",
"license": "MIT",
"source": {
"kind": "archive",
"url": "https://github.com/nghttp2/nghttp2/releases/download/v1.68.0/nghttp2-1.68.0.tar.gz",
"sha256": "2c16ffc588ad3f9e2613c3fad72db48ecb5ce15bc362fcc85b342e48daf51013"
},
"documents": [
{
"path": "COPYING",
"sha256": "6b94f3abc1aabd0c72a7c7d92a77f79dda7c8a0cb3df839a97890b4116a2de2a"
}
],
"recipe_sha256": "eec10214a906c663f74e3e6bf8952cddec39fec3cac710c10d8b9ea669c9ab79",
"recipe_path": "xmake-repo/packages/n/nghttp2/xmake.lua",
"package": "nghttp2",
"notice_prefixes": [
"lib/"
]
},
{
"id": "openfec",
"name": "OpenFEC",
"version": "1.4.2",
"license": "CeCILL-C 1.0 / BSD / CC-BY-SA-3.0",
"source": {
"kind": "archive",
"url": "https://github.com/kunkundi/openfec/archive/refs/tags/1.4.2.tar.gz",
"sha256": "5aa19dc56038f4b19927efb53ca28e896fb05f068a4715de512f0709420f03be"
},
"documents": [
{
"path": "LICENCE_CeCILL-C_V1-en.txt",
"sha256": "b48f5dc16f144cd79ddaadce3cc8ef183778a0b4f75ab4362e134b347ca20f3d",
"encoding": "iso-8859-1"
}
],
"recipe_sha256": "8644c6306ad6ba4c974dd949d56178c761b72d503fe7c4a577a23817effa6243",
"recipe_path": "deps/submodules/minirtc/thirdparty/openfec/xmake.lua",
"package": "openfec",
"notice_prefixes": [
"src/lib_common/",
"src/lib_stable/"
],
"notice_encodings": {
"src/lib_common/linear_binary_codes_utils/binary_matrix/of_hamming_weight.c": "gb18030",
"src/lib_common/linear_binary_codes_utils/binary_matrix/of_hamming_weight.h": "gb18030"
}
},
{
"id": "openssl3",
"name": "OpenSSL",
"version": "3.3.2",
"license": "Apache-2.0 and embedded notices",
"source": {
"kind": "archive",
"url": "https://github.com/openssl/openssl/archive/refs/tags/openssl-3.3.2.zip",
"sha256": "4cda357946f9dd5541b565dba35348d614288e88aeb499045018970c789c9d61"
},
"documents": [
{
"path": "LICENSE.txt",
"sha256": "7d5450cb2d142651b8afa315b5f238efc805dad827d91ba367d8516bc9d49e7a"
}
],
"recipe_sha256": "864c308edf59fd802babd45d064d8575635ecd577e829df2b2c7ac7a4d06d4d7",
"recipe_path": "deps/submodules/minirtc/thirdparty/openssl/xmake.lua",
"package": "openssl3",
"notice_prefixes": [
"crypto/",
"ssl/",
"include/",
"providers/"
]
},
{
"id": "libopus",
"name": "Opus",
"version": "1.5.1",
"license": "BSD-3-Clause and patent grants",
"source": {
"kind": "archive",
"url": "https://downloads.xiph.org/releases/opus/opus-1.5.1.tar.gz",
"sha256": "b84610959b8d417b611aa12a22565e0a3732097c6389d19098d844543e340f85"
},
"documents": [
{
"path": "COPYING",
"sha256": "01e1167d54a096d123cf6dfbbeb19587278845c6481d2d66d545669846079551"
}
],
"recipe_sha256": "c965643eb4e093858c27c402e6cde544620460f2169c89d8ff2bab01111d022b",
"recipe_path": "xmake-repo/packages/l/libopus/xmake.lua",
"package": "libopus",
"notice_prefixes": [
"src/",
"include/",
"celt/",
"silk/",
"dnn/"
]
},
{
"id": "pcre2",
"name": "PCRE2 / SLJIT",
"version": "10.47",
"license": "BSD-3-Clause / BSD-2-Clause",
"source": {
"kind": "archive",
"url": "https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.47/pcre2-10.47.tar.gz",
"sha256": "c08ae2388ef333e8403e670ad70c0a11f1eed021fd88308d7e02f596fcd9dc16"
},
"documents": [
{
"path": "LICENCE.md",
"sha256": "197d8a73ffee0d6b09adba2f9c677b5f5aede24edf89258a68e48248d010d811"
},
{
"path": "COPYING",
"sha256": "99272c55f3dcfa07a8a7e15a5c1a33096e4727de74241d65fa049fccfdd59507"
},
{
"path": "deps/sljit/LICENSE",
"sha256": "5f216505c0f6ea3273caec89e766eef93cdeb7bbb0c429f9360116d7c938feeb"
}
],
"recipe_sha256": "821f1fbc91434f4324be88eeea754db09c5d7be31446242764861ca2aee136b7",
"recipe_path": "xmake-repo/packages/p/pcre2/xmake.lua",
"package": "pcre2",
"notice_prefixes": [
"src/",
"deps/sljit/sljitLir.c",
"deps/sljit/sljitLir.h"
]
},
{
"id": "plog",
"name": "plog",
"version": "1.1.11",
"license": "MIT",
"source": {
"kind": "archive",
"url": "https://github.com/SergiusTheBest/plog/archive/refs/tags/1.1.11.tar.gz",
"sha256": "d60b8b35f56c7c852b7f00f58cbe9c1c2e9e59566c5b200512d0cdbb6309a7c2"
},
"documents": [
{
"path": "LICENSE",
"sha256": "e4d01796524cbc13b1571a1f0914823c6a23b316ba039e7014f47a4eef7fd4c3"
}
],
"recipe_sha256": "deb525d09b5b79351eeb524ab3bc6a3009e434974e585e5e8852c491d7fa07c8",
"recipe_path": "xmake-repo/packages/p/plog/xmake.lua",
"package": "plog",
"notice_prefixes": [
"include/"
]
},
{
"id": "proxy-libintl",
"name": "proxy-libintl (GLib libintl)",
"version": "33934de09af6a6627eb44e310a8079df009abdbb",
"license": "LGPL-2.0-or-later",
"source": {
"kind": "archive",
"url": "https://codeload.github.com/frida/proxy-libintl/tar.gz/33934de09af6a6627eb44e310a8079df009abdbb",
"sha256": "4ab5af85ec6124cb526a15c58bbc0877d241807e30855779808dbf9292d861aa",
"revision": "33934de09af6a6627eb44e310a8079df009abdbb"
},
"documents": [
{
"path": "COPYING",
"sha256": "d245807f90032872d1438d741ed21e2490e1175dc8aa3afa5ddb6c8e529b58e5"
}
],
"bundled_by": "glib",
"notice_prefixes": [
"src/",
"libintl.c",
"libintl.h"
]
},
{
"id": "spdlog",
"name": "spdlog / fmt 10.2.1",
"version": "v1.14.1",
"license": "MIT",
"source": {
"kind": "archive",
"url": "https://github.com/gabime/spdlog/archive/refs/tags/v1.14.1.zip",
"sha256": "429dfdf3afc1984feb59e414353c21c110bc79609f6d7899d52f6aa388646f6d"
},
"documents": [
{
"path": "LICENSE",
"sha256": "4ccecab18d1ff0b61174fe3d6c430541625d3ddb865b0d5887db296f883c76e7"
}
],
"recipe_sha256": "5b299effb366420b4920c5b89e6535b76173136d91ee01f8f7d3423486a93549",
"recipe_path": "xmake-repo/packages/s/spdlog/xmake.lua",
"package": "spdlog",
"notice_prefixes": [
"include/"
]
},
{
"id": "sqlite3",
"name": "SQLite",
"version": "3.50.0+400",
"license": "Public domain",
"source": {
"kind": "archive",
"url": "https://sqlite.org/2025/sqlite-autoconf-3500400.tar.gz",
"sha256": "a3db587a1b92ee5ddac2f66b3edb41b26f9c867275782d46c3a088977d6a5b18"
},
"documents": [],
"recipe_sha256": "a752522259dbd722ea79beaa01e5287429ccb37643ccffbecf5fa30ec4df30fd",
"recipe_path": "xmake-repo/packages/s/sqlite3/xmake.lua",
"package": "sqlite3",
"notice_prefixes": [
"sqlite3.h"
]
},
{
"id": "svt-av1",
"name": "SVT-AV1",
"version": "v3.0.2",
"license": "BSD-3-Clause-Clear / BSD-2-Clause, AOMedia patent license and embedded notices",
"source": {
"kind": "archive",
"url": "https://gitlab.com/AOMediaCodec/SVT-AV1/-/archive/efc905a7c2ed155b3654d7968173622734eeb0c0/SVT-AV1-efc905a7c2ed155b3654d7968173622734eeb0c0.tar.gz",
"sha256": "50a897d58470b06f99b386dc691ab63e802962421e6abdb75e4d234a3e73cd6a",
"revision": "efc905a7c2ed155b3654d7968173622734eeb0c0"
},
"documents": [
{
"path": "LICENSE.md",
"sha256": "0acc2fcb27472bdc9aaf8b71f37055bbdac4f54671b7d922f241bd7fcd0dd3e6"
},
{
"path": "LICENSE-BSD2.md",
"sha256": "c582fcbeb9cf972a60d298bfaf41dc5adff2dd14208946edcb9bb56a281e8438"
},
{
"path": "PATENTS.md",
"sha256": "20678ab10402659106dc4c147c97b2a6e94b5c0695415e15a8f195ebc3547922"
},
{
"path": "third_party/fastfeat/LICENSE",
"sha256": "043dcfd059386f9facd376351b2bd79325778744aa442177390cdfcca54babed"
},
{
"path": "third_party/safestringlib/LICENSE",
"sha256": "c0f99a208799ca20c9ccf31a30533d49334eb458dca5a3d14f9afe3e884e5d7d"
}
],
"package": "svt-av1",
"notice_prefixes": [
"Source/",
"third_party/fastfeat/",
"third_party/safestringlib/"
]
},
{
"id": "usrsctp",
"name": "usrsctp",
"version": "0.9.5+0",
"license": "BSD-3-Clause and embedded notices",
"source": {
"kind": "archive",
"url": "https://github.com/sctplab/usrsctp/archive/refs/tags/0.9.5.0.tar.gz",
"sha256": "260107caf318650a57a8caa593550e39bca6943e93f970c80d6c17e59d62cd92"
},
"documents": [
{
"path": "LICENSE.md",
"sha256": "fa53711b25af4b9a9b8dadfea3cb38166ec4b96760c8d62b284055554537d9ef"
}
],
"recipe_sha256": "e3dac7d1db8e5bc79d99851e2486371af19962a4b8fca6eaac3156c00eb73b92",
"recipe_path": "xmake-repo/packages/u/usrsctp/xmake.lua",
"package": "usrsctp",
"notice_prefixes": [
"usrsctplib/"
]
},
{
"id": "webrtc",
"name": "WebRTC-derived code in MiniRTC",
"version": "6286ae6c1c015609d3070cc139e5feea900caf7c",
"license": "BSD-3-Clause and patent grant",
"source": {
"kind": "repository",
"url": "https://github.com/kunkundi/minirtc",
"revision": "6286ae6c1c015609d3070cc139e5feea900caf7c"
},
"documents": [
{
"path": "deps/submodules/minirtc/thirdparty/webrtc/LICENSE",
"sha256": "ab00a482b6a3902e40211b43c5d0441962ea99b6cc7c25c0f243fa270b78d482"
},
{
"path": "deps/submodules/minirtc/thirdparty/webrtc/PATENTS",
"sha256": "01462e2068d1a04c2274f3389773014c14ed9bc3446b28303543bd3e3c064145"
}
]
},
{
"id": "websocketpp",
"name": "WebSocket++",
"version": "0.8.2",
"license": "BSD-3-Clause and embedded notices",
"source": {
"kind": "archive",
"url": "https://github.com/zaphoyd/websocketpp/archive/refs/tags/0.8.2.tar.gz",
"sha256": "6ce889d85ecdc2d8fa07408d6787e7352510750daa66b5ad44aacb47bea76755"
},
"documents": [
{
"path": "COPYING",
"sha256": "b31e597bc777d884a39663c11d0557a0af40780e1f451de11ea5103924a69667"
}
],
"recipe_sha256": "beac245a717c2bc3bc8d17265d9fc9b54f699f3833b11513f8723c4dbaffd539",
"recipe_path": "deps/submodules/minirtc/thirdparty/websocketpp/xmake.lua",
"package": "websocketpp",
"notice_prefixes": [
"websocketpp/"
]
},
{
"id": "zlib",
"name": "zlib",
"version": "v1.3.2",
"license": "Zlib",
"source": {
"kind": "archive",
"url": "https://github.com/madler/zlib/archive/refs/tags/v1.3.2.tar.gz",
"sha256": "b99a0b86c0ba9360ec7e78c4f1e43b1cbdf1e6936c8fa0f6835c0cd694a495a1"
},
"documents": [
{
"path": "LICENSE",
"sha256": "e32ff4e00d9d94930537635291da39e7e612703334bf6fde8c7f1686fe8a45a2"
}
],
"recipe_sha256": "7832bcb086709a0739ae1abeff035f7a103c7169dc8af61ebf91dd887fcbd054",
"recipe_path": "xmake-repo/packages/z/zlib/xmake.lua",
"package": "zlib",
"notice_prefixes": [
"zlib.h"
]
}
],
"bundle_sha256": "09376251bbda3f5a1f7112f15dcbb1259fef7898f85b45aacafa767348aafdc2",
"toolchain": {
"xmake_version": "3.1.1",
"xmake_repository_revision": "121109352da272cd1a3d4ed52f2dcda34f839955"
}
}
+20 -2
View File
@@ -57,6 +57,10 @@ fi
XMAKE_DEVELOPER_DIR="${DEVELOPER_DIR:-$(xcode-select -p)}"
XMAKE_TOOLCHAIN_BIN="${XMAKE_DEVELOPER_DIR}/Toolchains/XcodeDefault.xctoolchain/usr/bin"
run_xmake() {
local xmake_env=()
if [[ -n "${XMAKE_GLOBALDIR:-}" ]]; then
xmake_env+=("XMAKE_GLOBALDIR=${XMAKE_GLOBALDIR}")
fi
/usr/bin/env -i \
HOME="${HOME}" \
PATH="${XMAKE_TOOLCHAIN_BIN}:${PATH}" \
@@ -69,11 +73,16 @@ run_xmake() {
DEVELOPER_DIR="${XMAKE_DEVELOPER_DIR}" \
XMAKE_PKG_INSTALLDIR="${XMAKE_PKG_INSTALLDIR}" \
CROSSDESK_SOURCE_DIR="${REPO_DIR}" \
"${xmake_env[@]}" \
"${XMAKE_BIN}" "$@"
}
OUTPUT_DIR="${IOS_DIR}/Vendor/iphoneos/${CONFIG_NAME}"
OUTPUT_LIBRARY="${OUTPUT_DIR}/libCrossDeskMiniRTC.a"
XMAKE_REPOSITORY_DIR="$(run_xmake lua -c 'import("core.base.global"); print(path.join(global.directory(), "repositories", "xmake-repo"))')"
mkdir -p "${OUTPUT_DIR}"
print -r -- "${XMAKE_REPOSITORY_DIR}" > "${OUTPUT_DIR}/xmake-repository.txt"
run_xmake --version > "${OUTPUT_DIR}/xmake-version.txt"
MINIRTC_BUILD_DIR="${IOS_DIR}/.xmake/minirtc-build"
MINIRTC_LIBRARY="${MINIRTC_BUILD_DIR}/iphoneos/arm64/${MODE}/libminirtc.a"
WIRE_BUILD_DIR="${IOS_DIR}/.xmake/wire-build"
@@ -106,7 +115,8 @@ configure_xmake() {
clean_flags=(-c)
fi
run_xmake f -P "${project_dir}" "${clean_flags[@]}" -o "${build_dir}" \
-p iphoneos -a arm64 -m "${MODE}" --target_minver=16.0 -y "$@"
-p iphoneos -a arm64 -m "${MODE}" --target_minver=16.0 \
--policies=package.precompiled:n -y "$@"
mkdir -p "${build_dir}"
print -r -- "${CONFIG_SIGNATURE}" > "${stamp}"
}
@@ -148,6 +158,8 @@ TARGET_INFO="$(cd "${MINIRTC_DIR}" && TERM=dumb NO_COLOR=1 \
CLEAN_INFO="$(print -r -- "${TARGET_INFO}" | sed $'s/\033\\[[0-9;]*[[:alpha:]]//g')"
LINK_DIRS=("${(@f)$(print -r -- "${CLEAN_INFO}" | sed -nE 's|.*-> (/.*)/lib -> package.*|\1/lib|p' | sort -u)}")
print -r -- "${CLEAN_INFO}" > "${OUTPUT_DIR}/minirtc-target-info.txt"
REQUIRED_LINKS=(
nice glib-2.0 gobject-2.0 gmodule-2.0 gio-2.0 gthread-2.0 intl
gupnp-igd-1.6 gupnp-1.6 gssdp-1.6 soup-3.0 nghttp2 sqlite3 psl xml2
@@ -158,6 +170,7 @@ if [[ "${MINIRTC_ENABLE_AOM}" == "true" ]]; then
REQUIRED_LINKS+=(aom)
fi
DEPENDENCY_ARCHIVES=()
MERGE_ARCHIVES=()
for link_name in "${REQUIRED_LINKS[@]}"; do
archive_path=""
@@ -173,6 +186,11 @@ for link_name in "${REQUIRED_LINKS[@]}"; do
exit 66
fi
DEPENDENCY_ARCHIVES+=("${archive_path}")
# OpenSSL is linked separately as a static framework with its SDK privacy
# resources. Keep both archives in the input manifest for source validation.
if [[ "${link_name}" != "ssl" && "${link_name}" != "crypto" ]]; then
MERGE_ARCHIVES+=("${archive_path}")
fi
done
TEMP_LIBRARY="${OUTPUT_LIBRARY}.tmp"
@@ -204,7 +222,7 @@ if [[ -f "${OUTPUT_LIBRARY}" ]] && cmp -s "${TEMP_MANIFEST}" "${MERGE_MANIFEST}"
fi
/usr/bin/libtool -static -o "${TEMP_LIBRARY}" \
"${MINIRTC_LIBRARY}" "${DEPENDENCY_ARCHIVES[@]}" \
"${MINIRTC_LIBRARY}" "${MERGE_ARCHIVES[@]}" \
"${WIRE_LIBRARY}"
verify_no_openh264 "${TEMP_LIBRARY}"
mv -f "${TEMP_LIBRARY}" "${OUTPUT_LIBRARY}"
@@ -0,0 +1,213 @@
#!/usr/bin/env python3
"""Collect iOS notices from pinned source archives; validate them offline at build time."""
import argparse
from collections import defaultdict
from contextlib import contextmanager
import hashlib
import json
from pathlib import Path
import re
import subprocess
import sys
import tarfile
import urllib.request
import zipfile
IOS = Path(__file__).resolve().parents[1]
REPO = IOS.parents[1]
LOCK = IOS / "licenses/sources.json"
OUTPUT = IOS / "CrossDeskMobile/Resources/ThirdPartyLicenses.json"
COMMENT = re.compile(rb"/\*.*?\*/|(?://[^\n]*(?:\n|$))+", re.S)
RIGHTS = re.compile(rb"copyright|SPDX-License|permission is hereby|author disclaims|public domain", re.I)
def digest(data):
return hashlib.sha256(data).hexdigest()
def json_bytes(value):
return (json.dumps(value, ensure_ascii=False, indent=2) + "\n").encode()
def checked(data, expected, label):
if digest(data) != expected:
raise ValueError(f"Source checksum mismatch: {label}")
return data
def archive_path(component, cache):
source = component["source"]
target = cache / source["sha256"]
if target.exists():
checked(target.read_bytes(), source["sha256"], component["id"])
return target
cache.mkdir(parents=True, exist_ok=True)
# Xmake may remove extracted sources or COPYING; read the original archive.
candidates = (Path.home() / ".xmake/cache/packages").glob(
f"*/{component['id'][0]}/{component['id']}/{component['version']}/*")
for candidate in candidates:
if candidate.is_file() and candidate.name.endswith((".tar.gz", ".tar.xz", ".zip")):
data = candidate.read_bytes()
if digest(data) == source["sha256"]:
target.write_bytes(data)
return target
request = urllib.request.Request(source["url"], headers={"User-Agent": "CrossDesk-license-collector"})
with urllib.request.urlopen(request, timeout=90) as response:
data = checked(response.read(), source["sha256"], component["id"])
target.write_bytes(data)
return target
@contextmanager
def source_files(component, cache):
if component["source"]["kind"] == "repository":
yield {}, lambda name: (REPO / name).read_bytes()
return
path = archive_path(component, cache)
if zipfile.is_zipfile(path):
with zipfile.ZipFile(path) as archive:
names = {name.split("/", 1)[1]: name for name in archive.namelist()
if "/" in name and not name.endswith("/")}
yield names, lambda name: archive.read(names[name])
else:
with tarfile.open(path) as archive:
names = {member.name.split("/", 1)[1]: member.name for member in archive
if "/" in member.name and not member.isdir()}
# No filesystem extraction. tarfile resolves COPYING symlinks within
# the verified archive, including GLib's LICENSES directory.
yield names, lambda name: archive.extractfile(names[name]).read()
def component_documents(component, names, read):
documents = []
for item in component["documents"]:
data = checked(read(item["path"]), item["sha256"], item["path"])
text = data.decode(item.get("encoding", "utf-8"))
if "end_before" in item:
if item["end_before"] not in text:
raise ValueError(f"Missing notice boundary: {item['path']}")
text = text.split(item["end_before"], 1)[0]
documents.append({"title": item["path"], "text": text})
notices = defaultdict(list)
for name in sorted(names):
path = Path(name)
selected = any(name.startswith(prefix) for prefix in component.get("notice_prefixes", []))
selected |= component.get("notice_root_files", False) and "/" not in name
if not selected or path.suffix not in (".h", ".hpp", ".c", ".cc", ".cpp", ".S", ".s", ".inc"):
continue
if any(part.lower() in ("test", "tests", "benchmarks", "examples") for part in path.parts):
continue
for block in COMMENT.findall(read(name)[:16384]):
if RIGHTS.search(block):
# Preserve complete original comment blocks, including bundled
# fmt, semaphore and per-file copyright/permission exceptions.
encoding = component.get("notice_encodings", {}).get(name, "utf-8")
notices[block.decode(encoding)].append(name)
if notices:
documents.append({"title": "Source copyright and license notices", "text": "\n\n".join(
"Files: " + ", ".join(files) + "\n\n" + text for text, files in notices.items())})
if not documents:
raise ValueError(f"No notices collected for {component['id']}")
return documents
def generate(lock, cache):
components = []
for component in lock["components"]:
with source_files(component, cache) as (names, read):
documents = component_documents(component, names, read)
source = component["source"]
source_url = source["url"]
if source["kind"] == "repository" and source.get("revision"):
source_url += "/tree/" + source["revision"]
entry = {key: component[key] for key in ("id", "name", "version", "license")} | {
"sourceURL": source_url, "documents": documents}
recipe = component.get("recipe_path", "")
if recipe.startswith("deps/submodules/minirtc/"):
mini = next(c for c in lock["components"] if c["id"] == "minirtc")
entry["buildSourceURL"] = (mini["source"]["url"] + "/tree/" +
mini["source"]["revision"] + "/" +
str(Path(recipe).relative_to("deps/submodules/minirtc").parent))
components.append(entry)
print(f"Collected {component['name']} {component['version']}")
return json_bytes({"schemaVersion": 1, "components": components})
def verify(lock, inputs, target_info, xmake_repository=None, xmake_version=None):
checked(OUTPUT.read_bytes(), lock["bundle_sha256"], str(OUTPUT))
if xmake_version is not None:
version = re.search(r"xmake v(\d+\.\d+\.\d+)", xmake_version.read_text())
if not version or version[1] != lock["toolchain"]["xmake_version"]:
raise ValueError("Xmake version differs from the reviewed iOS toolchain; see apps/ios/README.md")
if xmake_repository is not None:
revision = subprocess.check_output(
["git", "-C", str(xmake_repository), "rev-parse", "HEAD"], text=True).strip()
if revision != lock["toolchain"]["xmake_repository_revision"]:
raise ValueError("Xmake package repository differs from the reviewed iOS source catalog")
changes = subprocess.check_output(
["git", "-C", str(xmake_repository), "status", "--porcelain", "--untracked-files=normal"],
text=True).strip()
if changes:
raise ValueError("Xmake package repository has local changes; use the unmodified pinned revision")
for component in lock["components"]:
recipe = component.get("recipe_path", "")
if recipe.startswith("deps/"):
checked((REPO / recipe).read_bytes(), component["recipe_sha256"], recipe)
elif recipe.startswith("xmake-repo/") and xmake_repository is not None:
checked((xmake_repository / Path(recipe).relative_to("xmake-repo")).read_bytes(),
component["recipe_sha256"], recipe)
if component["source"]["kind"] == "repository":
for item in component["documents"]:
checked((REPO / item["path"]).read_bytes(), item["sha256"], item["path"])
if inputs is None:
return
if target_info is None:
raise ValueError("--target-info is required with --inputs (includes header-only dependencies)")
known = {c["package"]: c["version"] for c in lock["components"] if "package" in c}
seen = {}
# Package names/versions come from resolved Xmake link and include paths,
# never from an inventory of all packages left in the installation cache.
pattern = r"/packages/[^/\s]+/([^/\s]+)/([^/\s]+)/[a-f0-9]+/"
for name, version in re.findall(pattern, inputs.read_text() + target_info.read_text()):
if name not in known or known[name] != version:
raise ValueError(f"Unreviewed iOS dependency: {name} {version}; regenerate licenses from its source")
seen[name] = version
if set(seen) != set(known):
raise ValueError(f"License/build dependency mismatch; missing: {sorted(set(known) - set(seen))}")
for line in inputs.read_text().splitlines():
sha, path = line.split(maxsplit=1)
checked(Path(path).read_bytes(), sha, path)
revision = subprocess.check_output(
["git", "-C", str(REPO / "deps/submodules/minirtc"), "rev-parse", "HEAD"], text=True).strip()
mini = next(c for c in lock["components"] if c["id"] == "minirtc")
if revision != mini["source"]["revision"]:
raise ValueError("MiniRTC revision changed; review embedded notices and update sources.json")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--generate", action="store_true", help="download/check pinned sources and regenerate the bundled JSON")
parser.add_argument("--cache", type=Path, default=Path.home() / ".cache/crossdesk/licenses")
parser.add_argument("--inputs", type=Path, help="native archive .inputs.sha256 manifest")
parser.add_argument("--target-info", type=Path, help="resolved Xmake minirtc target information")
parser.add_argument("--xmake-repository", type=Path, help="verify the active pinned package repository and recipes")
parser.add_argument("--xmake-version", type=Path, help="Xmake version output recorded by the native build")
args = parser.parse_args()
lock = json.loads(LOCK.read_text())
if args.generate:
data = generate(lock, args.cache)
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
OUTPUT.write_bytes(data)
lock["bundle_sha256"] = digest(data)
LOCK.write_bytes(json_bytes(lock))
verify(lock, args.inputs, args.target_info, args.xmake_repository, args.xmake_version)
print(f"Verified {len(lock['components'])} iOS license components")
if __name__ == "__main__":
try:
main()
except (OSError, ValueError, KeyError, subprocess.CalledProcessError) as error:
print(f"Third-party notices: {error}", file=sys.stderr)
sys.exit(1)
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Package source-built iOS OpenSSL as a static framework with privacy resources."""
import argparse
import hashlib
import json
from pathlib import Path
import plistlib
import subprocess
import sys
import tempfile
IOS = Path(__file__).resolve().parents[1]
PRIVACY = IOS / "OpenSSL/PrivacyInfo.xcprivacy"
# The reviewed iOS configuration removes these calls rather than declaring
# unrelated purposes for system entropy devices and certificate directories.
FORBIDDEN_APIS = {
"stat", "fstat", "lstat", "fstatat", "stat64", "fstat64", "lstat64",
"getattrlist", "fgetattrlist", "getattrlistbulk", "getdirentriesattr",
"statfs", "fstatfs", "statvfs", "fstatvfs", "getfsstat",
"mach_absolute_time", "mach_continuous_time", "sysctl",
}
def package(inputs, output):
catalog = json.loads((IOS / "CrossDeskMobile/Resources/ThirdPartyLicenses.json").read_text())
component = next(c for c in catalog["components"] if c["id"] == "openssl3")
archives = {}
for line in inputs.read_text().splitlines():
sha, name = line.split(maxsplit=1)
path = Path(name)
if path.name in {"libssl.a", "libcrypto.a"}:
if path.name in archives:
raise ValueError(f"Ambiguous OpenSSL input: {path.name}")
if hashlib.sha256(path.read_bytes()).hexdigest() != sha:
raise ValueError(f"OpenSSL input checksum mismatch: {path}")
archives[path.name] = (path, sha)
if set(archives) != {"libssl.a", "libcrypto.a"}:
raise ValueError("Both source-built OpenSSL archives must be present")
output.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix="openssl-framework-", dir=output.parent) as temp:
binary = Path(temp) / "OpenSSL"
subprocess.run(["/usr/bin/libtool", "-static", "-o", str(binary),
*(str(archives[name][0]) for name in sorted(archives))], check=True)
subprocess.run(["xcrun", "lipo", str(binary), "-verify_arch", "arm64"], check=True)
symbols = subprocess.check_output(["xcrun", "nm", "-u", str(binary)], text=True)
imports = {line.split()[-1].lstrip("_").split("$")[0]
for line in symbols.splitlines() if line.strip() and not line.endswith(":")}
unexpected = imports & FORBIDDEN_APIS
if unexpected:
raise ValueError(f"OpenSSL privacy configuration has unreviewed APIs: {sorted(unexpected)}")
# armcap.c queries only CPU instruction support/brand to select crypto
# implementations. This is not boot time, disk space or fingerprinting.
member = ""
for line in symbols.splitlines():
if line.endswith(":"):
member = line
elif "_sysctlbyname" in line and member != "libcrypto-lib-armcap.o:":
raise ValueError(f"Unreviewed OpenSSL sysctl caller: {member}")
# getrandom uses Darwin's getentropy via OpenSSL's existing DSO lookup;
# the random device fallback is excluded at compile time.
output.mkdir(parents=True, exist_ok=True)
files = {
"OpenSSL": binary.read_bytes(),
"PrivacyInfo.xcprivacy": PRIVACY.read_bytes(),
"Info.plist": plistlib.dumps({
"CFBundleIdentifier": "cn.crossdesk.OpenSSL",
"CFBundleName": "OpenSSL",
"CFBundleExecutable": "OpenSSL",
"CFBundlePackageType": "FMWK",
"CFBundleInfoDictionaryVersion": "6.0",
"CFBundleVersion": component["version"],
"CFBundleShortVersionString": component["version"],
"CFBundleSupportedPlatforms": ["iPhoneOS"],
"MinimumOSVersion": "16.0",
}),
"LICENSE.txt": "\n\n".join(d["text"] for d in component["documents"]).encode(),
"BuildMetadata.json": (json.dumps({
"version": component["version"],
"sourceURL": component["sourceURL"],
"buildSourceURL": component["buildSourceURL"],
"archives": {name: sha for name, (_, sha) in sorted(archives.items())},
"privacySHA256": hashlib.sha256(PRIVACY.read_bytes()).hexdigest(),
}, indent=2) + "\n").encode(),
}
for name, data in files.items():
target = output / name
if not target.exists() or target.read_bytes() != data:
target.write_bytes(data)
print(f"Packaged source-built OpenSSL {component['version']}: {output}")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--inputs", required=True, type=Path)
parser.add_argument("--output", required=True, type=Path)
args = parser.parse_args()
package(args.inputs, args.output)
if __name__ == "__main__":
try:
main()
except (OSError, ValueError, KeyError, StopIteration, subprocess.CalledProcessError) as error:
print(f"OpenSSL framework: {error}", file=sys.stderr)
sys.exit(1)
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Record the source corresponding to an iOS build without changing the checkout."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import subprocess
import sys
from urllib.parse import quote
REPO = Path(__file__).resolve().parents[3]
REPOSITORY_URL = "https://github.com/kunkundi/crossdesk"
MINIRTC_URL = "https://github.com/kunkundi/minirtc"
def git(*args, root=REPO):
return subprocess.check_output(["git", "-C", str(root), *args], text=True).strip()
def source_metadata(require_release=False, tag=None):
revision = git("rev-parse", "HEAD")
dirty = bool(git("status", "--porcelain", "--untracked-files=normal", "--ignore-submodules=none"))
tags = git("tag", "--points-at", "HEAD").splitlines()
if tag:
if tag not in tags:
raise ValueError(f"Source tag {tag!r} does not point to the build commit")
else:
# 'latest' is a moving alias, never a corresponding-source identifier.
versions = sorted(t for t in tags if t.startswith("v"))
tag = versions[0] if versions else None
if require_release and (dirty or not tag or not tag.startswith("v")):
raise ValueError("Release source must be a clean checkout of a version tag, with matching submodules")
mini_revision = git("rev-parse", "HEAD", root=REPO / "deps/submodules/minirtc")
lock_path = REPO / "apps/ios/licenses/sources.json"
lock = json.loads(lock_path.read_text())
ref = quote(tag if tag and not dirty else revision, safe="")
return {
"schemaVersion": 1,
"repositoryURL": REPOSITORY_URL,
"revision": revision,
"tag": tag,
"isModified": dirty,
"sourceURL": f"{REPOSITORY_URL}/tree/{ref}",
"buildInstructionsURL": f"{REPOSITORY_URL}/blob/{ref}/apps/ios/README.md",
"privacyPolicyURL": f"{REPOSITORY_URL}/blob/{ref}/PRIVACY.md",
"miniRTCRevision": mini_revision,
"miniRTCSourceURL": f"{MINIRTC_URL}/tree/{mini_revision}",
"licenseCatalogSHA256": lock["bundle_sha256"],
"sourceCatalogSHA256": hashlib.sha256(lock_path.read_bytes()).hexdigest(),
"toolchain": lock["toolchain"],
}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", required=True, type=Path)
parser.add_argument("--tag", default=os.environ.get("CROSSDESK_SOURCE_TAG") or None)
parser.add_argument("--require-release", action="store_true",
default=os.environ.get("CROSSDESK_RELEASE_BUILD") == "YES")
args = parser.parse_args()
metadata = source_metadata(args.require_release, args.tag)
data = (json.dumps(metadata, ensure_ascii=False, indent=2) + "\n").encode()
args.output.parent.mkdir(parents=True, exist_ok=True)
if not args.output.exists() or args.output.read_bytes() != data:
args.output.write_bytes(data)
print(f"Recorded iOS source: {metadata['revision']} (modified: {metadata['isModified']})")
if __name__ == "__main__":
try:
main()
except (OSError, ValueError, subprocess.CalledProcessError) as error:
print(f"iOS source metadata: {error}", file=sys.stderr)
sys.exit(1)
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Check release resources and known restricted imports in a built iOS app."""
import argparse
import hashlib
import json
from pathlib import Path
import plistlib
import re
import subprocess
import sys
def verify(app, require_release=False):
info = plistlib.loads((app / "Info.plist").read_bytes())
binary = app / info["CFBundleExecutable"]
subprocess.run(["xcrun", "lipo", str(binary), "-verify_arch", "arm64"], check=True)
symbols = subprocess.check_output(["xcrun", "nm", "-u", str(binary)], text=True)
imports = set(line.split()[-1] for line in symbols.splitlines() if line.strip())
disk_apis = {"_statfs", "_fstatfs", "_statvfs", "_fstatvfs", "_getfsstat"}
if imports & disk_apis:
raise ValueError(f"Unreviewed disk-space APIs remain in iOS app: {sorted(imports & disk_apis)}")
defined = subprocess.check_output(["xcrun", "nm", "-g", str(binary)], text=True)
if re.search(r"\b_Wels(?:CreateDecoder|CreateSVCEncoder|GetCodecVersion)\b", defined):
raise ValueError("OpenH264 must not be present in the iOS app")
if re.search(r"\b_of_(?:hweight\w*|hw8table|popcount_3)\b", defined):
raise ValueError("OpenFEC's optional CC-BY-SA-3.0 hamming-weight code requires a separate distribution review")
privacy = plistlib.loads((app / "PrivacyInfo.xcprivacy").read_bytes())
reasons = {entry["NSPrivacyAccessedAPIType"]: entry["NSPrivacyAccessedAPITypeReasons"]
for entry in privacy["NSPrivacyAccessedAPITypes"]}
required = {
"NSPrivacyAccessedAPICategoryUserDefaults": {"CA92.1"},
"NSPrivacyAccessedAPICategoryFileTimestamp": {"C617.1", "3B52.1"},
"NSPrivacyAccessedAPICategorySystemBootTime": {"35F9.1", "8FFB.1"},
}
for category, codes in required.items():
if set(reasons.get(category, [])) != codes:
raise ValueError(f"Missing or changed reviewed privacy reasons: {category}")
data = (app / "ThirdPartyLicenses.json").read_bytes()
catalog = json.loads(data)
ids = {c["id"] for c in catalog["components"]}
if not {"crossdesk", "minirtc", "openfec"} <= ids or "openh264" in ids:
raise ValueError("Unexpected iOS license catalog")
openssl = next(c for c in catalog["components"] if c["id"] == "openssl3")
framework = app / "Frameworks/OpenSSL.framework"
sdk_info = plistlib.loads((framework / "Info.plist").read_bytes())
sdk_privacy_data = (framework / "PrivacyInfo.xcprivacy").read_bytes()
sdk_privacy = plistlib.loads(sdk_privacy_data)
expected_privacy = {
"NSPrivacyTracking": False,
"NSPrivacyTrackingDomains": [],
"NSPrivacyCollectedDataTypes": [],
"NSPrivacyAccessedAPITypes": [],
}
if sdk_privacy != expected_privacy:
raise ValueError("OpenSSL SDK privacy declaration differs from the reviewed iOS configuration")
sdk_build = json.loads((framework / "BuildMetadata.json").read_text())
if (sdk_info["CFBundleShortVersionString"] != openssl["version"]
or sdk_build["version"] != openssl["version"]
or sdk_build["sourceURL"] != openssl["sourceURL"]
or sdk_build["buildSourceURL"] != openssl["buildSourceURL"]
or sdk_build["privacySHA256"] != hashlib.sha256(sdk_privacy_data).hexdigest()):
raise ValueError("OpenSSL SDK resources do not match the source/license catalog")
if (framework / "LICENSE.txt").read_text() != "\n\n".join(d["text"] for d in openssl["documents"]):
raise ValueError("OpenSSL SDK license resource differs from its source notices")
embedded_binary = framework / sdk_info["CFBundleExecutable"]
# Xcode removes the archive and may generate a small codeless-framework
# stub. It must not ship a second static archive or dynamic OpenSSL copy.
if embedded_binary.exists():
if embedded_binary.read_bytes().startswith(b"!<arch>\n"):
raise ValueError("Xcode must remove the static OpenSSL archive when embedding its resources")
sdk_symbols = subprocess.check_output(["xcrun", "nm", "-gU", str(embedded_binary)], text=True)
if re.search(r"\b_(?:SSL_new|OPENSSL_init_crypto)\b", sdk_symbols):
raise ValueError("Unexpected dynamic OpenSSL implementation in resource framework")
linked = subprocess.check_output(["xcrun", "otool", "-L", str(binary)], text=True)
if "OpenSSL.framework" in linked:
raise ValueError("OpenSSL must be statically linked into the application")
source = json.loads((app / "SourceMetadata.json").read_text())
if source["licenseCatalogSHA256"] != hashlib.sha256(data).hexdigest():
raise ValueError("Bundled licenses differ from the recorded source metadata")
if require_release and (source["isModified"] or not source["tag"]):
raise ValueError("The app was not built from an unmodified version tag")
if not re.fullmatch(r"[0-9a-f]{40}", source["revision"]):
raise ValueError("Invalid source revision")
print("Verified arm64 app, source/license resources, app privacy declarations and OpenSSL SDK privacy packaging")
print("Signing, App Store validation and license authorization require separate review.")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("app", type=Path)
parser.add_argument("--require-release", action="store_true")
args = parser.parse_args()
verify(args.app, args.require_release)
if __name__ == "__main__":
try:
main()
except (OSError, ValueError, KeyError, subprocess.CalledProcessError) as error:
print(f"iOS bundle verification: {error}", file=sys.stderr)
sys.exit(1)