field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

commit 1e65049b9f2b2fcaf16b32e8b8354cf462417c65
parent 7465b598a80fe63955e56a3b09d0afcfdb7e3b5b
Author: triesap <tyson@radroots.org>
Date:   Fri,  2 Oct 2026 14:52:30 +0000

privacy: align current product declarations

- Declare linked public content and actual required API reasons
- Adopt the qualified local file capacity owner at its exact pin
- Package the approved support contact and honest removal language
- Qualify installed manifests permissions and fail-closed checks

Diffstat:
MPackage.resolved | 2+-
MPackage.swift | 3++-
MREADME.md | 27+++++++++++++++++++++++++++
MTera.xcodeproj/project.pbxproj | 6+++++-
MTera.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2+-
MTera/Info.plist | 6+++---
MTera/Resources/PrivacyInfo.xcprivacy | 79++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
MTera/Views/TeraSettingsView.swift | 1+
ATera/Views/TeraSupportSettingsSection.swift | 27+++++++++++++++++++++++++++
MTeraFFI/provenance.json | 26+++++++++++++-------------
MTeraFFI/source.lock | 4++--
MTeraFFI/source/aarch64-apple-darwin.json | 38+++++++++++++++++++++++++-------------
MTeraFFI/source/aarch64-apple-ios-sim.json | 38+++++++++++++++++++++++++-------------
MTeraFFI/source/aarch64-apple-ios.json | 38+++++++++++++++++++++++++-------------
ATeraTests/TeraPrivacyPackagingTests.swift | 47+++++++++++++++++++++++++++++++++++++++++++++++
Mproject.yml | 3++-
Mrelease/provenance.json | 16++++++++--------
Mrelease/sbom.cdx.json | 10+++++-----
Mscripts/package_contract.py | 22+++++++++-------------
Ascripts/package_privacy.py | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/swift-quality.sh | 2++
Ascripts/test_package_privacy.py | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/verify-package-contract.sh | 1+
Mtest-fixtures/legacy-identifiers.v1.json | 30+++++++++++++++++++++++++++++-
Mtest-fixtures/maintainability-baseline.v1.json | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
25 files changed, 644 insertions(+), 122 deletions(-)

diff --git a/Package.resolved b/Package.resolved @@ -6,7 +6,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/radrootslabs/apple_kit.git", "state" : { - "revision" : "1126a77ed87387719a6c6d3b4ce580582af29553" + "revision" : "d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2" } }, { diff --git a/Package.swift b/Package.swift @@ -14,7 +14,7 @@ let package = Package( dependencies: [ .package( url: "https://github.com/radrootslabs/apple_kit.git", - revision: "1126a77ed87387719a6c6d3b4ce580582af29553" + revision: "d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2" ), ], targets: [ @@ -216,6 +216,7 @@ let package = Package( "Views/TeraRecoveryUITestSurface.swift", "Views/TeraSupportingViews.swift", "Views/TeraSettingsView.swift", + "Views/TeraSupportSettingsSection.swift", "Views/TeraStorageSettingsSection.swift", "Views/TeraContextPicker.swift", "Views/TeraTodayDiscoveryView.swift", diff --git a/README.md b/README.md @@ -429,3 +429,30 @@ files. Draft staging, pending operations, transfer receipts and backup media are not cleanup targets. Invalidation must persist first: if the store is completely full, free device space before trying again. Cache cleanup cannot resolve the independent bounded transfer-receipt envelope limit. + + +Public posts and profile updates are linked to the public Nostr key and may be +retained by relays and other people. Only deliberately included location text +(such as a public venue or address) is posted; camera/library images have +sensitive metadata removed before staging. The app does not automatically +publish device location and has no tracking or automatic telemetry endpoint. +Its privacy manifest covers public names, identifiers, deliberately supplied +addresses, photos and other posted content for application functionality. + +File metadata is accessed only for owned storage. The generic file owner +checks available capacity locally before a write and reports the existing +insufficient-space outcome. This check is conservative and does not reserve +space or replace actual write/quota error handling. Capacity values stay local. + +Diagnostics exports contain bounded status codes and counts; prepare and review +one before explicitly sharing it. [Radroots Support](mailto:support@radroots.org) +is the canonical contact for support, privacy and removal inquiries. Settings +opens the system email handler only after an explicit choice; no report or attachment +is sent automatically. Provisioning/monitoring and operational reporting drills +remain distribution prerequisites, not engineering-test outcomes. + +Removing a local signing key does not erase public copies. Review and stage any +desired signed deletion requests before removing the key. Requests and relay +acknowledgements cannot prove erasure by every recipient. The implemented local +removal flow preserves its existing source and user-presence checks; it does not +claim a decentralized-account policy exemption or App Store approval. diff --git a/Tera.xcodeproj/project.pbxproj b/Tera.xcodeproj/project.pbxproj @@ -144,6 +144,7 @@ EF7293C553BF1DB45EEEBD49 /* local-social-personas.v1.json in Resources */ = {isa = PBXBuildFile; fileRef = 27D9D40699A01FFB02F30B11 /* local-social-personas.v1.json */; }; F13090A3350CAE9CA3FFF3F3 /* TeraLateSigningTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 217E44A3221756EC904702D6 /* TeraLateSigningTests.swift */; }; F14217EA66C2A9397E36E3FA /* TeraEditingOperationProtectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC9B19425D3374B899485C17 /* TeraEditingOperationProtectionTests.swift */; }; + F211D24BC279D3FF4AC12094 /* TeraPrivacyPackagingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EFD344F51F015AE6775F03D3 /* TeraPrivacyPackagingTests.swift */; }; F4AF91E71B691FE30C191852 /* TeraAddObservationStartupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44A4460928B20A21A94BECCE /* TeraAddObservationStartupTests.swift */; }; F8A87EA68BEF06EA7F0838D0 /* TeraRecoveryUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5928D5CAC9F8EF904547DF8 /* TeraRecoveryUITests.swift */; }; F8C87782857DC930D47E6A90 /* TeraRuntimeResourceTaskTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D01C023E258AC3B3B1C27D /* TeraRuntimeResourceTaskTests.swift */; }; @@ -304,6 +305,7 @@ EDE158F5F1E490847A5196EB /* TeraDiagnosticPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraDiagnosticPolicyTests.swift; sourceTree = "<group>"; }; EE1A300DCBB15E1911EA7D6C /* TeraCoordinateAdmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraCoordinateAdmissionTests.swift; sourceTree = "<group>"; }; EFBAA57ABEAA4B84C73EE8E8 /* TeraScopedMediaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraScopedMediaTests.swift; sourceTree = "<group>"; }; + EFD344F51F015AE6775F03D3 /* TeraPrivacyPackagingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraPrivacyPackagingTests.swift; sourceTree = "<group>"; }; EFECCBF9A84D1D65544C6094 /* TeraTodayContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraTodayContextTests.swift; sourceTree = "<group>"; }; F06622DFA682235F4BFA0841 /* TeraKeyRemovalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraKeyRemovalTests.swift; sourceTree = "<group>"; }; F0A51F9AF2E0803264E07FBB /* TeraRuntimeResourceFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TeraRuntimeResourceFixtures.swift; sourceTree = "<group>"; }; @@ -444,6 +446,7 @@ 9B10B1F3C7FCD9A72F04FC00 /* TeraOpenedMediaTests.swift */, 9F8B650ED2D867EF94248F6F /* TeraOperationRecoveryTests.swift */, E1BF69ADD9540F6800FD64B0 /* TeraPermissionRecoveryTests.swift */, + EFD344F51F015AE6775F03D3 /* TeraPrivacyPackagingTests.swift */, 7AF60EF9DCFDC4ECE77AED67 /* TeraProductStartupTests.swift */, 3B954B7006CC5904C98138C6 /* TeraPublicationEvidenceTests.swift */, A195B277BEDC31DE3A0D7601 /* TeraPublicationNavigationTests.swift */, @@ -778,6 +781,7 @@ 6FED2D2255CC702B33CDC760 /* TeraOpenedMediaTests.swift in Sources */, 026981088C27CB79A49D9833 /* TeraOperationRecoveryTests.swift in Sources */, C20BDCF02F461EE53B14B53E /* TeraPermissionRecoveryTests.swift in Sources */, + F211D24BC279D3FF4AC12094 /* TeraPrivacyPackagingTests.swift in Sources */, C46F8EB5B196FFDD37E0279A /* TeraProductStartupTests.swift in Sources */, B9E397E21B1654C108AF74BF /* TeraPublicDisclosureTests.swift in Sources */, 093A6AC6C9B724E465507004 /* TeraPublicationEvidenceTests.swift in Sources */, @@ -1180,7 +1184,7 @@ repositoryURL = "https://github.com/radrootslabs/apple_kit.git"; requirement = { kind = revision; - revision = 1126a77ed87387719a6c6d3b4ce580582af29553; + revision = d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Tera.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Tera.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,7 +6,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/radrootslabs/apple_kit.git", "state" : { - "revision" : "1126a77ed87387719a6c6d3b4ce580582af29553" + "revision" : "d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2" } }, { diff --git a/Tera/Info.plist b/Tera/Info.plist @@ -21,7 +21,7 @@ <key>CFBundleVersion</key> <string>1</string> <key>NSCameraUsageDescription</key> - <string>Tera uses the camera only when you choose to add a farm photo.</string> + <string>Tera uses the camera only when you choose to add a photo to a public post.</string> <key>NSFaceIDUsageDescription</key> <string>Tera uses Face ID only to unlock the local Nostr identity or approve a post or media upload you choose to sign.</string> <key>NSLocalNetworkUsageDescription</key> @@ -46,7 +46,6 @@ </array> </dict> </dict> - <key>UILaunchScreen</key> <dict> <key>UIColorName</key> @@ -67,7 +66,6 @@ <string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeRight</string> </array> - <key>TERA_IOS_RUNTIME_MODE</key> <string>$(TERA_IOS_RUNTIME_MODE)</string> <key>TERA_IOS_NOSTR_RELAY_URLS</key> @@ -76,5 +74,7 @@ <string>$(TERA_IOS_KEYCHAIN_SERVICE_PREFIX)</string> <key>TERA_IOS_BLOSSOM_ORIGINS</key> <string>$(TERA_IOS_BLOSSOM_ORIGINS)</string> + <key>TeraSupportEmail</key> + <string>support@radroots.org</string> </dict> </plist> diff --git a/Tera/Resources/PrivacyInfo.xcprivacy b/Tera/Resources/PrivacyInfo.xcprivacy @@ -7,11 +7,88 @@ <key>NSPrivacyTrackingDomains</key> <array/> <key>NSPrivacyCollectedDataTypes</key> - <array/> + <array> + <dict> + <key>NSPrivacyCollectedDataType</key> + <string>NSPrivacyCollectedDataTypeName</string> + <key>NSPrivacyCollectedDataTypeLinked</key> + <true/> + <key>NSPrivacyCollectedDataTypeTracking</key> + <false/> + <key>NSPrivacyCollectedDataTypePurposes</key> + <array> + <string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string> + </array> + </dict> + <dict> + <key>NSPrivacyCollectedDataType</key> + <string>NSPrivacyCollectedDataTypeUserID</string> + <key>NSPrivacyCollectedDataTypeLinked</key> + <true/> + <key>NSPrivacyCollectedDataTypeTracking</key> + <false/> + <key>NSPrivacyCollectedDataTypePurposes</key> + <array> + <string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string> + </array> + </dict> + <dict> + <key>NSPrivacyCollectedDataType</key> + <string>NSPrivacyCollectedDataTypePhysicalAddress</string> + <key>NSPrivacyCollectedDataTypeLinked</key> + <true/> + <key>NSPrivacyCollectedDataTypeTracking</key> + <false/> + <key>NSPrivacyCollectedDataTypePurposes</key> + <array> + <string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string> + </array> + </dict> + <dict> + <key>NSPrivacyCollectedDataType</key> + <string>NSPrivacyCollectedDataTypePhotosorVideos</string> + <key>NSPrivacyCollectedDataTypeLinked</key> + <true/> + <key>NSPrivacyCollectedDataTypeTracking</key> + <false/> + <key>NSPrivacyCollectedDataTypePurposes</key> + <array> + <string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string> + </array> + </dict> + <dict> + <key>NSPrivacyCollectedDataType</key> + <string>NSPrivacyCollectedDataTypeOtherUserContent</string> + <key>NSPrivacyCollectedDataTypeLinked</key> + <true/> + <key>NSPrivacyCollectedDataTypeTracking</key> + <false/> + <key>NSPrivacyCollectedDataTypePurposes</key> + <array> + <string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string> + </array> + </dict> + </array> <key>NSPrivacyAccessedAPITypes</key> <array> <dict> <key>NSPrivacyAccessedAPIType</key> + <string>NSPrivacyAccessedAPICategoryFileTimestamp</string> + <key>NSPrivacyAccessedAPITypeReasons</key> + <array> + <string>C617.1</string> + </array> + </dict> + <dict> + <key>NSPrivacyAccessedAPIType</key> + <string>NSPrivacyAccessedAPICategoryDiskSpace</string> + <key>NSPrivacyAccessedAPITypeReasons</key> + <array> + <string>E174.1</string> + </array> + </dict> + <dict> + <key>NSPrivacyAccessedAPIType</key> <string>NSPrivacyAccessedAPICategoryUserDefaults</string> <key>NSPrivacyAccessedAPITypeReasons</key> <array> diff --git a/Tera/Views/TeraSettingsView.swift b/Tera/Views/TeraSettingsView.swift @@ -192,6 +192,7 @@ struct TeraSettingsView: View { .disabled(addStore.isCheckingBlossom || addStore.blossomConfiguration == nil) .accessibilityIdentifier("radroots.settings.retry.blossom") } + TeraSupportSettingsSection() Section("Runtime") { LabeledContent("Crate", value: snapshot.crateName) LabeledContent("Version", value: snapshot.crateVersion) diff --git a/Tera/Views/TeraSupportSettingsSection.swift b/Tera/Views/TeraSupportSettingsSection.swift @@ -0,0 +1,27 @@ +import Foundation +import SwiftUI + +/// Read the approved packaged contact. No message or diagnostics are sent +/// until the person chooses an external action and submits it themselves. +enum TeraSupportContact { + static func mailURL(email: String?) -> URL? { + guard email == "support@radroots.org" else { return nil } + return URL(string: "mailto:support@radroots.org") + } + + static var packagedMailURL: URL? { + mailURL(email: Bundle.main.object(forInfoDictionaryKey: "TeraSupportEmail") as? String) + } +} + +struct TeraSupportSettingsSection: View { + var body: some View { + Section("Support and privacy") { + if let contact = TeraSupportContact.packagedMailURL { + Link("Contact Radroots Support", destination: contact) + .accessibilityIdentifier("tera.settings.support.contact") + } + Text("Share diagnostics only after reviewing an export. Removing a local signing key does not erase copies already held by relays or other people.") + } + } +} diff --git a/TeraFFI/provenance.json b/TeraFFI/provenance.json @@ -92,19 +92,19 @@ "sha256": "d7b5bce06f98b202c676e2022377d006459be8572aa1bccf4d902e5247b69238" }, { - "bytes": 138031, + "bytes": 138526, "path": "source/aarch64-apple-darwin.json", - "sha256": "c227dc0072bf5f333f0da087fd72906cb657d8b3be4442c032292b6ebc625321" + "sha256": "1492ea27ad780f31f2ff9c8fed2ccb736e82d8b735c487dddf4f3a776d289373" }, { - "bytes": 137875, + "bytes": 138370, "path": "source/aarch64-apple-ios-sim.json", - "sha256": "267f13ec71cfccda3d2d9bc6600fab446fde9fda4b7e0afd31011aeabe96785e" + "sha256": "dc03ba11437cc803c1321c10f686e24ea7cdbef7057d1827e578c6bb52b73c5f" }, { - "bytes": 137871, + "bytes": 138366, "path": "source/aarch64-apple-ios.json", - "sha256": "c1dc9e9a167368169dfd74cef909497c204a41c7da76d00a12f5ba4fbbcf7c85" + "sha256": "c0d7ae5c5101fa98040bc0d533e8703afdd98cb8f8b569d04f790a69cc198e2f" } ], "language": "swift", @@ -112,7 +112,7 @@ "schema": "radroots.artifact-manifest.v2", "source": { "repository": "https://github.com/radrootslabs/tera", - "tree": "8f9abc83606e9996524f519b8bfb18dc3348590a" + "tree": "5b2ddf887733ca6e86f26893ccc3c852ac2dee67" }, "source_records": { "aarch64-apple-darwin": "source/aarch64-apple-darwin.json", @@ -174,19 +174,19 @@ "sha256": "5d74060f97e52dad69ab7567c16d6b6b34acfab846f468450d89e2394a8a69d5" }, { - "bytes": 138031, + "bytes": 138526, "path": "TeraFFI/source/aarch64-apple-darwin.json", - "sha256": "c227dc0072bf5f333f0da087fd72906cb657d8b3be4442c032292b6ebc625321" + "sha256": "1492ea27ad780f31f2ff9c8fed2ccb736e82d8b735c487dddf4f3a776d289373" }, { - "bytes": 137875, + "bytes": 138370, "path": "TeraFFI/source/aarch64-apple-ios-sim.json", - "sha256": "267f13ec71cfccda3d2d9bc6600fab446fde9fda4b7e0afd31011aeabe96785e" + "sha256": "dc03ba11437cc803c1321c10f686e24ea7cdbef7057d1827e578c6bb52b73c5f" }, { - "bytes": 137871, + "bytes": 138366, "path": "TeraFFI/source/aarch64-apple-ios.json", - "sha256": "c1dc9e9a167368169dfd74cef909497c204a41c7da76d00a12f5ba4fbbcf7c85" + "sha256": "c0d7ae5c5101fa98040bc0d533e8703afdd98cb8f8b569d04f790a69cc198e2f" } ], "schema": "tera.installed-native-artifacts.v1" diff --git a/TeraFFI/source.lock b/TeraFFI/source.lock @@ -1,7 +1,7 @@ schema = "tera.installed-source.v1" repository = "https://github.com/radrootslabs/tera" -source_tree = "8f9abc83606e9996524f519b8bfb18dc3348590a" -manifest_sha256 = "feaf4681644f041ce4bf841cf1e3c29b03bba15c9de9858f1c0a1ccf63419993" +source_tree = "5b2ddf887733ca6e86f26893ccc3c852ac2dee67" +manifest_sha256 = "c0aebdf85e1295346b78d3b2b384cb0e64f5a9f0a2348e08f70398b6471fe862" source_date_epoch = 1787871027 [foundation] diff --git a/TeraFFI/source/aarch64-apple-darwin.json b/TeraFFI/source/aarch64-apple-darwin.json @@ -2812,10 +2812,16 @@ "sha256": "fff99e755f35fa9f00708427b1b29290c0ecc2733c0ea070f5807ec7c37f6f11" }, "scripts/package_contract.py": { - "bytes": 25795, - "git_blob": "d68f5df2cc26d252be264fe55ece761070d02bee", + "bytes": 25574, + "git_blob": "ea94df8c0f106abc85e47295c2da7b023c168123", "mode": "100755", - "sha256": "b88df13f9c0da49fc535cf9997c668ec1357e9dbf6db2b472ac29fc3304c888c" + "sha256": "f00372d39a5f9849b6a535ed829a50f52810c3ac9e0695e80a71c802e23f0394" + }, + "scripts/package_privacy.py": { + "bytes": 2718, + "git_blob": "a05951b332833f03520884ab6877b89f4ca88120", + "mode": "100644", + "sha256": "4540c5254ceb68634dc4d028d1056ad40558c4919c6d2be0d2d4c31958f46cbe" }, "scripts/persona-verifier.sh": { "bytes": 240, @@ -2854,10 +2860,10 @@ "sha256": "1803572abdb2e22bedd275724edae5f09c523b132bc59086aacca0f5183905d0" }, "scripts/swift-quality.sh": { - "bytes": 2080, - "git_blob": "65b81671c82fe4102e6599532b4d2d802cedf4a0", + "bytes": 2143, + "git_blob": "0dea0e430dcc4e246bc951938faaa25db2596074", "mode": "100755", - "sha256": "15c98b26fdae1e6db8032b3201a8ab7e820800c721f0a4791ff374f81e6a004d" + "sha256": "9bb734eafe3c375ef30732e59a837ec345da438d96dc7882ec7f6ee1de43c13a" }, "scripts/test_app_dependency_graph.py": { "bytes": 4007, @@ -2913,6 +2919,12 @@ "mode": "100644", "sha256": "9073f8edaa80c71f7c1f718c4dcff954a30ac42218fbd19378803ecfb9b3b33f" }, + "scripts/test_package_privacy.py": { + "bytes": 4467, + "git_blob": "f1f821a0db6742ae2de3425c98d19297eb3463e9", + "mode": "100644", + "sha256": "651fc803fe3e3dd1636aec75f9ea79d81b9407317f93a02e4074a8369558bc33" + }, "scripts/test_xcode_selection.py": { "bytes": 4745, "git_blob": "b633afbccd765a106e2ad591881926a9add542ac", @@ -2920,10 +2932,10 @@ "sha256": "a917dfbc1d8ecac7d1539670651e4b468059bc77dfe31aeefbc6c6eb21bebbfb" }, "scripts/verify-package-contract.sh": { - "bytes": 1496, - "git_blob": "b21f0d5e48bcedd71460f7c0f4cf0c8f1070b27a", + "bytes": 1532, + "git_blob": "88f15a643269a8851d7fef68fe14208e71e2d843", "mode": "100755", - "sha256": "8ecb7b9fc1997e1cdb37d642d6dbfa59194bf9182218aceab470077f721fcd85" + "sha256": "028a8310c4a61a4cc04f1d0f86b8b953413b8efef5cb2059fbb0c84338483428" }, "scripts/xcode.sh": { "bytes": 22437, @@ -2932,13 +2944,13 @@ "sha256": "4e174e14512c1b12994dea63f58bf39d234d96c3bf245eb5d7eaa0fcf2b4fd24" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 186929, - "git_blob": "39860837f76433ef305fde969d9162fefa98e579", + "bytes": 187553, + "git_blob": "c0ce4d1c11ad8683bf9c45029db89c77dc8fc4f7", "mode": "100644", - "sha256": "0e7b8f85e58a1b34c27ebbaa03b21f30e54efd9f4998462dda8b2bba93cb54df" + "sha256": "e62d91d5b47a9565ca7cc46008be68cc780236cda46b7acb83234e59f538bcc7" } }, "policy": "staged_inputs", - "tree": "8f9abc83606e9996524f519b8bfb18dc3348590a" + "tree": "5b2ddf887733ca6e86f26893ccc3c852ac2dee67" } } diff --git a/TeraFFI/source/aarch64-apple-ios-sim.json b/TeraFFI/source/aarch64-apple-ios-sim.json @@ -2808,10 +2808,16 @@ "sha256": "fff99e755f35fa9f00708427b1b29290c0ecc2733c0ea070f5807ec7c37f6f11" }, "scripts/package_contract.py": { - "bytes": 25795, - "git_blob": "d68f5df2cc26d252be264fe55ece761070d02bee", + "bytes": 25574, + "git_blob": "ea94df8c0f106abc85e47295c2da7b023c168123", "mode": "100755", - "sha256": "b88df13f9c0da49fc535cf9997c668ec1357e9dbf6db2b472ac29fc3304c888c" + "sha256": "f00372d39a5f9849b6a535ed829a50f52810c3ac9e0695e80a71c802e23f0394" + }, + "scripts/package_privacy.py": { + "bytes": 2718, + "git_blob": "a05951b332833f03520884ab6877b89f4ca88120", + "mode": "100644", + "sha256": "4540c5254ceb68634dc4d028d1056ad40558c4919c6d2be0d2d4c31958f46cbe" }, "scripts/persona-verifier.sh": { "bytes": 240, @@ -2850,10 +2856,10 @@ "sha256": "1803572abdb2e22bedd275724edae5f09c523b132bc59086aacca0f5183905d0" }, "scripts/swift-quality.sh": { - "bytes": 2080, - "git_blob": "65b81671c82fe4102e6599532b4d2d802cedf4a0", + "bytes": 2143, + "git_blob": "0dea0e430dcc4e246bc951938faaa25db2596074", "mode": "100755", - "sha256": "15c98b26fdae1e6db8032b3201a8ab7e820800c721f0a4791ff374f81e6a004d" + "sha256": "9bb734eafe3c375ef30732e59a837ec345da438d96dc7882ec7f6ee1de43c13a" }, "scripts/test_app_dependency_graph.py": { "bytes": 4007, @@ -2909,6 +2915,12 @@ "mode": "100644", "sha256": "9073f8edaa80c71f7c1f718c4dcff954a30ac42218fbd19378803ecfb9b3b33f" }, + "scripts/test_package_privacy.py": { + "bytes": 4467, + "git_blob": "f1f821a0db6742ae2de3425c98d19297eb3463e9", + "mode": "100644", + "sha256": "651fc803fe3e3dd1636aec75f9ea79d81b9407317f93a02e4074a8369558bc33" + }, "scripts/test_xcode_selection.py": { "bytes": 4745, "git_blob": "b633afbccd765a106e2ad591881926a9add542ac", @@ -2916,10 +2928,10 @@ "sha256": "a917dfbc1d8ecac7d1539670651e4b468059bc77dfe31aeefbc6c6eb21bebbfb" }, "scripts/verify-package-contract.sh": { - "bytes": 1496, - "git_blob": "b21f0d5e48bcedd71460f7c0f4cf0c8f1070b27a", + "bytes": 1532, + "git_blob": "88f15a643269a8851d7fef68fe14208e71e2d843", "mode": "100755", - "sha256": "8ecb7b9fc1997e1cdb37d642d6dbfa59194bf9182218aceab470077f721fcd85" + "sha256": "028a8310c4a61a4cc04f1d0f86b8b953413b8efef5cb2059fbb0c84338483428" }, "scripts/xcode.sh": { "bytes": 22437, @@ -2928,13 +2940,13 @@ "sha256": "4e174e14512c1b12994dea63f58bf39d234d96c3bf245eb5d7eaa0fcf2b4fd24" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 186929, - "git_blob": "39860837f76433ef305fde969d9162fefa98e579", + "bytes": 187553, + "git_blob": "c0ce4d1c11ad8683bf9c45029db89c77dc8fc4f7", "mode": "100644", - "sha256": "0e7b8f85e58a1b34c27ebbaa03b21f30e54efd9f4998462dda8b2bba93cb54df" + "sha256": "e62d91d5b47a9565ca7cc46008be68cc780236cda46b7acb83234e59f538bcc7" } }, "policy": "staged_inputs", - "tree": "8f9abc83606e9996524f519b8bfb18dc3348590a" + "tree": "5b2ddf887733ca6e86f26893ccc3c852ac2dee67" } } diff --git a/TeraFFI/source/aarch64-apple-ios.json b/TeraFFI/source/aarch64-apple-ios.json @@ -2808,10 +2808,16 @@ "sha256": "fff99e755f35fa9f00708427b1b29290c0ecc2733c0ea070f5807ec7c37f6f11" }, "scripts/package_contract.py": { - "bytes": 25795, - "git_blob": "d68f5df2cc26d252be264fe55ece761070d02bee", + "bytes": 25574, + "git_blob": "ea94df8c0f106abc85e47295c2da7b023c168123", "mode": "100755", - "sha256": "b88df13f9c0da49fc535cf9997c668ec1357e9dbf6db2b472ac29fc3304c888c" + "sha256": "f00372d39a5f9849b6a535ed829a50f52810c3ac9e0695e80a71c802e23f0394" + }, + "scripts/package_privacy.py": { + "bytes": 2718, + "git_blob": "a05951b332833f03520884ab6877b89f4ca88120", + "mode": "100644", + "sha256": "4540c5254ceb68634dc4d028d1056ad40558c4919c6d2be0d2d4c31958f46cbe" }, "scripts/persona-verifier.sh": { "bytes": 240, @@ -2850,10 +2856,10 @@ "sha256": "1803572abdb2e22bedd275724edae5f09c523b132bc59086aacca0f5183905d0" }, "scripts/swift-quality.sh": { - "bytes": 2080, - "git_blob": "65b81671c82fe4102e6599532b4d2d802cedf4a0", + "bytes": 2143, + "git_blob": "0dea0e430dcc4e246bc951938faaa25db2596074", "mode": "100755", - "sha256": "15c98b26fdae1e6db8032b3201a8ab7e820800c721f0a4791ff374f81e6a004d" + "sha256": "9bb734eafe3c375ef30732e59a837ec345da438d96dc7882ec7f6ee1de43c13a" }, "scripts/test_app_dependency_graph.py": { "bytes": 4007, @@ -2909,6 +2915,12 @@ "mode": "100644", "sha256": "9073f8edaa80c71f7c1f718c4dcff954a30ac42218fbd19378803ecfb9b3b33f" }, + "scripts/test_package_privacy.py": { + "bytes": 4467, + "git_blob": "f1f821a0db6742ae2de3425c98d19297eb3463e9", + "mode": "100644", + "sha256": "651fc803fe3e3dd1636aec75f9ea79d81b9407317f93a02e4074a8369558bc33" + }, "scripts/test_xcode_selection.py": { "bytes": 4745, "git_blob": "b633afbccd765a106e2ad591881926a9add542ac", @@ -2916,10 +2928,10 @@ "sha256": "a917dfbc1d8ecac7d1539670651e4b468059bc77dfe31aeefbc6c6eb21bebbfb" }, "scripts/verify-package-contract.sh": { - "bytes": 1496, - "git_blob": "b21f0d5e48bcedd71460f7c0f4cf0c8f1070b27a", + "bytes": 1532, + "git_blob": "88f15a643269a8851d7fef68fe14208e71e2d843", "mode": "100755", - "sha256": "8ecb7b9fc1997e1cdb37d642d6dbfa59194bf9182218aceab470077f721fcd85" + "sha256": "028a8310c4a61a4cc04f1d0f86b8b953413b8efef5cb2059fbb0c84338483428" }, "scripts/xcode.sh": { "bytes": 22437, @@ -2928,13 +2940,13 @@ "sha256": "4e174e14512c1b12994dea63f58bf39d234d96c3bf245eb5d7eaa0fcf2b4fd24" }, "test-fixtures/legacy-identifiers.v1.json": { - "bytes": 186929, - "git_blob": "39860837f76433ef305fde969d9162fefa98e579", + "bytes": 187553, + "git_blob": "c0ce4d1c11ad8683bf9c45029db89c77dc8fc4f7", "mode": "100644", - "sha256": "0e7b8f85e58a1b34c27ebbaa03b21f30e54efd9f4998462dda8b2bba93cb54df" + "sha256": "e62d91d5b47a9565ca7cc46008be68cc780236cda46b7acb83234e59f538bcc7" } }, "policy": "staged_inputs", - "tree": "8f9abc83606e9996524f519b8bfb18dc3348590a" + "tree": "5b2ddf887733ca6e86f26893ccc3c852ac2dee67" } } diff --git a/TeraTests/TeraPrivacyPackagingTests.swift b/TeraTests/TeraPrivacyPackagingTests.swift @@ -0,0 +1,47 @@ +import CoreFoundation +import Foundation +@testable import TeraApp +import XCTest + +final class TeraPrivacyPackagingTests: XCTestCase { + func testInstalledBundleContainsLinkedPublicContentAndReviewedReasonsWithoutTracking() throws { + let url = try XCTUnwrap(Bundle.main.url(forResource: "PrivacyInfo", withExtension: "xcprivacy")) + let value = try PropertyListSerialization.propertyList(from: Data(contentsOf: url), format: nil) + let manifest = try XCTUnwrap(value as? [String: Any]) + let tracking = try XCTUnwrap(manifest["NSPrivacyTracking"] as? NSNumber) + XCTAssertEqual(CFGetTypeID(tracking), CFBooleanGetTypeID()) + XCTAssertFalse(tracking.boolValue) + XCTAssertEqual(manifest["NSPrivacyTrackingDomains"] as? [String], []) + let dataTypes = try XCTUnwrap(manifest["NSPrivacyCollectedDataTypes"] as? [[String: Any]]) + XCTAssertEqual(dataTypes.compactMap { $0["NSPrivacyCollectedDataType"] as? String }, [ + "NSPrivacyCollectedDataTypeName", "NSPrivacyCollectedDataTypeUserID", + "NSPrivacyCollectedDataTypePhysicalAddress", "NSPrivacyCollectedDataTypePhotosorVideos", + "NSPrivacyCollectedDataTypeOtherUserContent", + ]) + for type in dataTypes { + XCTAssertEqual(type["NSPrivacyCollectedDataTypeLinked"] as? Bool, true) + XCTAssertEqual(type["NSPrivacyCollectedDataTypeTracking"] as? Bool, false) + XCTAssertEqual(type["NSPrivacyCollectedDataTypePurposes"] as? [String], ["NSPrivacyCollectedDataTypePurposeAppFunctionality"]) + } + let apis = try XCTUnwrap(manifest["NSPrivacyAccessedAPITypes"] as? [[String: Any]]) + XCTAssertEqual(apis.compactMap { $0["NSPrivacyAccessedAPIType"] as? String }, [ + "NSPrivacyAccessedAPICategoryFileTimestamp", "NSPrivacyAccessedAPICategoryDiskSpace", "NSPrivacyAccessedAPICategoryUserDefaults", + ]) + XCTAssertEqual(apis.compactMap { $0["NSPrivacyAccessedAPITypeReasons"] as? [String] }, [["C617.1"], ["E174.1"], ["CA92.1"]]) + } + + func testInstalledPurposeAndSupportContactDescribeCurrentPublicPosting() throws { + XCTAssertEqual(Bundle.main.object(forInfoDictionaryKey: "NSCameraUsageDescription") as? String, + "Tera uses the camera only when you choose to add a photo to a public post.") + XCTAssertEqual(Bundle.main.object(forInfoDictionaryKey: "TeraSupportEmail") as? String, "support@radroots.org") + let mail = try XCTUnwrap(TeraSupportContact.packagedMailURL) + XCTAssertEqual(mail.absoluteString, "mailto:support@radroots.org") + } + + func testContactRefusesUnreviewedOrInjectedRecipients() { + for email: String? in [nil, "", "operator@example.invalid", "support@radroots.org?subject=private", " support@radroots.org"] { + XCTAssertNil(TeraSupportContact.mailURL(email: email)) + } + XCTAssertEqual(TeraSupportContact.mailURL(email: "support@radroots.org")?.absoluteString, "mailto:support@radroots.org") + } +} diff --git a/project.yml b/project.yml @@ -13,7 +13,7 @@ packages: path: . RadrootsKit: url: https://github.com/radrootslabs/apple_kit.git - revision: 1126a77ed87387719a6c6d3b4ce580582af29553 + revision: d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2 targets: Tera: @@ -164,6 +164,7 @@ targets: - path: TeraTests/TeraCapacitySettingsTests.swift - path: TeraTests/TeraLifecycleTests.swift - path: TeraTests/TeraDiagnosticPolicyTests.swift + - path: TeraTests/TeraPrivacyPackagingTests.swift - path: TeraTests/TeraClockTests.swift - path: TeraTests/TeraCalendarFFICompatibilityTests.swift - path: TeraTests/TeraCalendarTimingTests.swift diff --git a/release/provenance.json b/release/provenance.json @@ -2,11 +2,11 @@ "artifacts": { "app_api_sha256": "020924097c0d7efc33128cb8fd3d3b2026d95f57c44da71880e585aff80f070b", "ffi_api_sha256": "5d74060f97e52dad69ab7567c16d6b6b34acfab846f468450d89e2394a8a69d5", - "ffi_provenance_sha256": "feaf4681644f041ce4bf841cf1e3c29b03bba15c9de9858f1c0a1ccf63419993", - "info_plist_sha256": "15ef08b1cdd1096cfb9eeaf5be5bf8f814807a7ca9350bbbb47860fa72ec13ef", - "privacy_manifest_sha256": "a331d51864743ebe4e00dd22360b4a538b6b3ac26a6b3eb54094e60a36959a12", - "sbom_sha256": "9e243176e3eaec256f404207d284314e8c96d3c6861077acb8e760640047ee2f", - "xcode_project_sha256": "6ad5a40e50a198c65afa5e5f78d229c951b618d8ee98e56e49aa5ee7596ff763" + "ffi_provenance_sha256": "c0aebdf85e1295346b78d3b2b384cb0e64f5a9f0a2348e08f70398b6471fe862", + "info_plist_sha256": "b55010dd73b34bf5ad910a888be0ea1ae45f2a5c14329fa6a57059a317bdbd42", + "privacy_manifest_sha256": "0f2103257ba596fbcaeb1d6a4dffe3ae021e84c010178852c6bf500a8cfb0f19", + "sbom_sha256": "f0a15c2c9ad7799539fbc52fd1fdbcf17f4046b6f86cfe0a9b8980adc0079e10", + "xcode_project_sha256": "a14bc2bd4f2b69020947721014432389804c0a8797c70aa90172810407649a2e" }, "disposition": "unsigned", "platforms": [ @@ -21,9 +21,9 @@ "consumer_source_lock_sha256": "cb660a5c2ba1a8e0ef5629152364a9be86eca83220fb40d5bff7f800bf9c32a0", "lib_revision": "189c49b74b4bafc142b00b76b296477931139e72", "source_date_epoch": 1787871027, - "swift_package_lock_sha256": "322eaec80d4b85ef9eb20da95ff95c4d26d8555ca358ba0c8eb890adf0b6671e", - "tera_ffi_source_tree": "8f9abc83606e9996524f519b8bfb18dc3348590a", - "xcode_package_lock_sha256": "4e29fed46339d5e382fa78ec85b61be4296455ece4ddc40602d793c049e673b3" + "swift_package_lock_sha256": "8aec408132602a249111a65903c985cca5898ec316a45916d88ba756f08ea40d", + "tera_ffi_source_tree": "5b2ddf887733ca6e86f26893ccc3c852ac2dee67", + "xcode_package_lock_sha256": "3656728f8251f3ea644aa150d154819766470ca7bd4f85b7c51fdf301d361d16" }, "version": "0.1.0-alpha" } diff --git a/release/sbom.cdx.json b/release/sbom.cdx.json @@ -7856,7 +7856,7 @@ "version": "1.0.23" }, { - "bom-ref": "swift:apple_kit@1126a77ed87387719a6c6d3b4ce580582af29553?source=https://github.com/radrootslabs/apple_kit.git", + "bom-ref": "swift:apple_kit@d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2?source=https://github.com/radrootslabs/apple_kit.git", "name": "apple_kit", "properties": [ { @@ -7869,11 +7869,11 @@ }, { "name": "radroots.package.revision", - "value": "1126a77ed87387719a6c6d3b4ce580582af29553" + "value": "d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2" } ], "type": "library", - "version": "1126a77ed87387719a6c6d3b4ce580582af29553" + "version": "d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2" }, { "bom-ref": "swift:swift-secp256k1@e70a10e036a55fffea31568f0af92d69b6d449cd?source=https://github.com/21-DOT-DEV/swift-secp256k1.git", @@ -10770,14 +10770,14 @@ "cargo:tera_core@0.1.0-alpha?source=workspace", "cargo:tera_ffi@0.1.0-alpha?source=workspace", "cargo:tera_wasm@0.1.0-alpha?source=workspace", - "swift:apple_kit@1126a77ed87387719a6c6d3b4ce580582af29553?source=https://github.com/radrootslabs/apple_kit.git", + "swift:apple_kit@d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2?source=https://github.com/radrootslabs/apple_kit.git", "swift:swift-secp256k1@e70a10e036a55fffea31568f0af92d69b6d449cd?source=https://github.com/21-DOT-DEV/swift-secp256k1.git" ], "ref": "pkg:generic/tera@0.1.0-alpha" }, { "dependsOn": [], - "ref": "swift:apple_kit@1126a77ed87387719a6c6d3b4ce580582af29553?source=https://github.com/radrootslabs/apple_kit.git" + "ref": "swift:apple_kit@d6b568213bf8d32fa79d8e94a78076a0b5b0e5d2?source=https://github.com/radrootslabs/apple_kit.git" }, { "dependsOn": [], diff --git a/scripts/package_contract.py b/scripts/package_contract.py @@ -19,6 +19,7 @@ from typing import Any import app_dependency_graph import legacy_identifiers +import package_privacy MAX_CONTRACT_BYTES = 2 * 1024 * 1024 GIT_REVISION = re.compile(r"^[0-9a-f]{40}$") @@ -266,19 +267,10 @@ def _apple_dependency_revision(dependency: object) -> object | None: def _validate_privacy(document: dict[str, Any]) -> None: - _exact(document.get("NSPrivacyTracking"), False, "privacy tracking") - _exact(document.get("NSPrivacyTrackingDomains"), [], "privacy tracking domains") - _exact(document.get("NSPrivacyCollectedDataTypes"), [], "privacy collected data") - _exact( - document.get("NSPrivacyAccessedAPITypes"), - [ - { - "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryUserDefaults", - "NSPrivacyAccessedAPITypeReasons": ["CA92.1"], - } - ], - "privacy accessed APIs", - ) + try: + package_privacy.validate_manifest(document) + except ValueError as error: + raise PackageContractError(str(error)) from error def _validate_app_plist(document: dict[str, Any]) -> None: @@ -290,6 +282,10 @@ def _validate_app_plist(document: dict[str, Any]) -> None: value = document.get(key) if not isinstance(value, str) or not value.strip(): raise PackageContractError(f"required plist purpose is absent: {key}") + try: + package_privacy.validate_purposes(document) + except ValueError as error: + raise PackageContractError(str(error)) from error _exact( document.get("NSAppTransportSecurity"), {"NSAllowsLocalNetworking": True}, diff --git a/scripts/package_privacy.py b/scripts/package_privacy.py @@ -0,0 +1,70 @@ +"""Owned privacy and contact declarations for the implemented public product.""" + +from __future__ import annotations + +from typing import Any + +SUPPORT_EMAIL = "support@radroots.org" +CAMERA_PURPOSE = ( + "Tera uses the camera only when you choose to add a photo to a public post." +) + + +def manifest() -> dict[str, Any]: + """Publicly posted identity/profile/content/media remain linked off device.""" + data_types = [ + "NSPrivacyCollectedDataTypeName", + "NSPrivacyCollectedDataTypeUserID", + "NSPrivacyCollectedDataTypePhysicalAddress", + "NSPrivacyCollectedDataTypePhotosorVideos", + "NSPrivacyCollectedDataTypeOtherUserContent", + ] + return { + "NSPrivacyTracking": False, + "NSPrivacyTrackingDomains": [], + "NSPrivacyCollectedDataTypes": [ + { + "NSPrivacyCollectedDataType": value, + "NSPrivacyCollectedDataTypeLinked": True, + "NSPrivacyCollectedDataTypeTracking": False, + "NSPrivacyCollectedDataTypePurposes": [ + "NSPrivacyCollectedDataTypePurposeAppFunctionality" + ], + } + for value in data_types + ], + "NSPrivacyAccessedAPITypes": [ + { + "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryFileTimestamp", + "NSPrivacyAccessedAPITypeReasons": ["C617.1"], + }, + { + "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryDiskSpace", + "NSPrivacyAccessedAPITypeReasons": ["E174.1"], + }, + { + "NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryUserDefaults", + "NSPrivacyAccessedAPITypeReasons": ["CA92.1"], + }, + ], + } + + +def validate_manifest(document: dict[str, Any]) -> None: + # JSON preserves boolean versus numeric types; Python equality aliases 0 + # and False and cannot independently validate a privacy declaration. + import json + + try: + actual = json.dumps(document, sort_keys=True, allow_nan=False) + except (TypeError, ValueError) as error: + raise ValueError("privacy manifest contains unsupported values") from error + if actual != json.dumps(manifest(), sort_keys=True): + raise ValueError("privacy manifest differs from implemented public product") + + +def validate_purposes(document: dict[str, Any]) -> None: + if document.get("NSCameraUsageDescription") != CAMERA_PURPOSE: + raise ValueError("camera purpose differs from implemented public posting") + if document.get("TeraSupportEmail") != SUPPORT_EMAIL: + raise ValueError("support contact differs from approved canonical contact") diff --git a/scripts/swift-quality.sh b/scripts/swift-quality.sh @@ -30,6 +30,8 @@ readonly -a PYTHON_QUALITY_PATHS=( scripts/test_ffi_provenance.py scripts/maintainability_ratchet.py scripts/package_contract.py + scripts/package_privacy.py + scripts/test_package_privacy.py scripts/legacy_identifiers.py scripts/test_legacy_identifiers.py scripts/app_dependency_graph.py diff --git a/scripts/test_package_privacy.py b/scripts/test_package_privacy.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +import copy +import sys +import unittest +from pathlib import Path + +SCRIPTS = Path(__file__).resolve().parent +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + +import package_contract as contract # noqa: E402 +import package_privacy as privacy # noqa: E402 + + +class PrivacyContractTests(unittest.TestCase): + def test_real_manifest_and_permission_contact_inputs_match_the_product( + self, + ) -> None: + root = SCRIPTS.parent + contract._validate_privacy( + contract._read_plist(root / "Tera/Resources/PrivacyInfo.xcprivacy") + ) + contract._validate_app_plist(contract._read_plist(root / "Tera/Info.plist")) + + def test_tracking_and_linkage_cannot_hide_behind_numeric_boolean_aliases( + self, + ) -> None: + for numeric in [0, 0.0, 1, 1.0]: + for target in ["tracking", "linked", "entry_tracking"]: + with self.subTest(numeric=numeric, target=target): + value = privacy.manifest() + if target == "tracking": + value["NSPrivacyTracking"] = numeric + elif target == "linked": + value["NSPrivacyCollectedDataTypes"][0][ + "NSPrivacyCollectedDataTypeLinked" + ] = numeric + else: + value["NSPrivacyCollectedDataTypes"][0][ + "NSPrivacyCollectedDataTypeTracking" + ] = numeric + with self.assertRaises(contract.PackageContractError): + contract._validate_privacy(value) + + def test_missing_data_type_reason_or_tracking_declaration_fails_closed( + self, + ) -> None: + base = privacy.manifest() + for key in base: + with self.subTest(key=key): + value = copy.deepcopy(base) + del value[key] + with self.assertRaises(contract.PackageContractError): + contract._validate_privacy(value) + for inventory in ["NSPrivacyCollectedDataTypes", "NSPrivacyAccessedAPITypes"]: + for index in range(len(base[inventory])): + with self.subTest(inventory=inventory, index=index): + value = copy.deepcopy(base) + value[inventory].pop(index) + with self.assertRaises(contract.PackageContractError): + contract._validate_privacy(value) + + def test_duplicate_foreign_purpose_or_unreviewed_api_reason_fails_closed( + self, + ) -> None: + for inventory in ["NSPrivacyCollectedDataTypes", "NSPrivacyAccessedAPITypes"]: + value = privacy.manifest() + value[inventory].append(copy.deepcopy(value[inventory][0])) + with self.assertRaises(contract.PackageContractError): + contract._validate_privacy(value) + for kind, key, replacement in [ + ( + "NSPrivacyCollectedDataTypes", + "NSPrivacyCollectedDataTypePurposes", + ["NSPrivacyCollectedDataTypePurposeAnalytics"], + ), + ( + "NSPrivacyAccessedAPITypes", + "NSPrivacyAccessedAPITypeReasons", + ["3B52.1"], + ), + ]: + value = privacy.manifest() + value[kind][0][key] = replacement + with self.assertRaises(contract.PackageContractError): + contract._validate_privacy(value) + + def test_stale_permission_and_foreign_contact_cannot_pass_nonempty_string_checks( + self, + ) -> None: + root = SCRIPTS.parent + base = contract._read_plist(root / "Tera/Info.plist") + for key, bad in [ + ("NSCameraUsageDescription", "Add a farm photo"), + ("TeraSupportEmail", "operator@example.invalid"), + ("TeraSupportEmail", None), + ]: + value = {**base, key: bad} + with self.assertRaises(contract.PackageContractError): + contract._validate_app_plist(value) + + def test_unsupported_or_nonfinite_values_return_a_stable_rejection(self) -> None: + for value in [b"private", float("nan"), float("inf")]: + document = privacy.manifest() + document["unexpected"] = value + with self.assertRaises(contract.PackageContractError): + contract._validate_privacy(document) diff --git a/scripts/verify-package-contract.sh b/scripts/verify-package-contract.sh @@ -14,6 +14,7 @@ sh "$repo_root/scripts/ffi-provenance.sh" contract-check uv run --project "$python_project" --offline --frozen \ python -m unittest \ scripts/test_package_contract.py \ + scripts/test_package_privacy.py \ scripts/test_legacy_identifiers.py \ scripts/test_app_dependency_graph.py \ scripts/test_ffi_provenance.py \ diff --git a/test-fixtures/legacy-identifiers.v1.json b/test-fixtures/legacy-identifiers.v1.json @@ -126,9 +126,17 @@ }, { "identifier": "Radroots", - "category": "retired_producer_rejection", + "category": "public_namespace", "occurrences": [ { + "path": "README.md", + "count": 1 + }, + { + "path": "Tera/Views/TeraSupportSettingsSection.swift", + "count": 1 + }, + { "path": "scripts/test_legacy_identifiers.py", "count": 1 } @@ -4265,14 +4273,30 @@ "count": 2 }, { + "path": "README.md", + "count": 1 + }, + { "path": "Tera/Config/Base.xcconfig", "count": 2 }, { + "path": "Tera/Info.plist", + "count": 1 + }, + { "path": "Tera/State/TeraConfigurationStore.swift", "count": 1 }, { + "path": "Tera/Views/TeraSupportSettingsSection.swift", + "count": 2 + }, + { + "path": "TeraTests/TeraPrivacyPackagingTests.swift", + "count": 6 + }, + { "path": "TeraTests/TeraStateMigrationTests.swift", "count": 1 }, @@ -4297,6 +4321,10 @@ "count": 2 }, { + "path": "scripts/package_privacy.py", + "count": 1 + }, + { "path": "scripts/test_local_social_fixture.py", "count": 8 } diff --git a/test-fixtures/maintainability-baseline.v1.json b/test-fixtures/maintainability-baseline.v1.json @@ -8,47 +8,143 @@ "python_function_complexity": 10 }, "swift_file_exception": [ - {"path": "Tera/Runtime/TeraGeneratedRuntimeBackend.swift", "maximum_lines": 1188}, - {"path": "Tera/Runtime/TeraRuntimeClient.swift", "maximum_lines": 820}, - {"path": "Tera/Runtime/TeraRuntimeModels.swift", "maximum_lines": 1154}, - {"path": "Tera/State/TeraAddStore.swift", "maximum_lines": 783}, - {"path": "Tera/State/TeraConfigurationStore.swift", "maximum_lines": 759}, - {"path": "TeraTests/TeraAddStoreTests.swift", "maximum_lines": 1366}, - {"path": "TeraTests/TeraStateMigrationTests.swift", "maximum_lines": 718}, - {"path": "TeraUITests/TeraRemoteQualificationUITests.swift", "maximum_lines": 1992} + { + "path": "Tera/Runtime/TeraGeneratedRuntimeBackend.swift", + "maximum_lines": 1188 + }, + { + "path": "Tera/Runtime/TeraRuntimeClient.swift", + "maximum_lines": 820 + }, + { + "path": "Tera/Runtime/TeraRuntimeModels.swift", + "maximum_lines": 1154 + }, + { + "path": "Tera/State/TeraAddStore.swift", + "maximum_lines": 783 + }, + { + "path": "Tera/State/TeraConfigurationStore.swift", + "maximum_lines": 759 + }, + { + "path": "TeraTests/TeraAddStoreTests.swift", + "maximum_lines": 1366 + }, + { + "path": "TeraTests/TeraStateMigrationTests.swift", + "maximum_lines": 718 + }, + { + "path": "TeraUITests/TeraRemoteQualificationUITests.swift", + "maximum_lines": 1992 + } ], "python_file_exception": [ - {"path": "scripts/local-social-fixture.py", "maximum_lines": 2626}, - {"path": "scripts/test_local_social_fixture.py", "maximum_lines": 1188} + { + "path": "scripts/local-social-fixture.py", + "maximum_lines": 2626 + }, + { + "path": "scripts/test_local_social_fixture.py", + "maximum_lines": 1188 + } ], "python_complexity_exception": [ - {"function": "scripts/local-social-fixture.py:FixtureState._publish_persona_event", "maximum_complexity": 13}, - {"function": "scripts/local-social-fixture.py:FixtureState.upload", "maximum_complexity": 14}, - {"function": "scripts/local-social-fixture.py:RelayHandler.handle", "maximum_complexity": 26}, - {"function": "scripts/local-social-fixture.py:bounded_directory_inventory", "maximum_complexity": 12}, - {"function": "scripts/local-social-fixture.py:directory_digest", "maximum_complexity": 16}, - {"function": "scripts/local-social-fixture.py:exact_persona_test_node", "maximum_complexity": 11}, - {"function": "scripts/local-social-fixture.py:load_exported_persona_attachments", "maximum_complexity": 31}, - {"function": "scripts/local-social-fixture.py:matches", "maximum_complexity": 22}, - {"function": "scripts/local-social-fixture.py:photo_attempt_matches", "maximum_complexity": 12}, - {"function": "scripts/local-social-fixture.py:reconstruct_persona_result_v2", "maximum_complexity": 66}, - {"function": "scripts/local-social-fixture.py:simulator_metadata", "maximum_complexity": 23}, - {"function": "scripts/local-social-fixture.py:valid_blossom_authorization", "maximum_complexity": 29}, - {"function": "scripts/local-social-fixture.py:valid_bud11_server_domain", "maximum_complexity": 12}, - {"function": "scripts/local-social-fixture.py:valid_nostr_event", "maximum_complexity": 16}, - {"function": "scripts/local-social-fixture.py:validate_persona_attempt_evidence", "maximum_complexity": 59}, - {"function": "scripts/local-social-fixture.py:validate_persona_evidence", "maximum_complexity": 31}, - {"function": "scripts/local-social-fixture.py:validate_persona_result", "maximum_complexity": 43}, - {"function": "scripts/local-social-fixture.py:validate_persona_suite", "maximum_complexity": 28}, - {"function": "scripts/local-social-fixture.py:verify_bip340", "maximum_complexity": 11}, - {"function": "scripts/local-social-fixture.py:verify_persona", "maximum_complexity": 18}, - {"function": "scripts/test_local_social_fixture.py:mutate_bud11_event", "maximum_complexity": 29} + { + "function": "scripts/local-social-fixture.py:FixtureState._publish_persona_event", + "maximum_complexity": 13 + }, + { + "function": "scripts/local-social-fixture.py:FixtureState.upload", + "maximum_complexity": 14 + }, + { + "function": "scripts/local-social-fixture.py:RelayHandler.handle", + "maximum_complexity": 26 + }, + { + "function": "scripts/local-social-fixture.py:bounded_directory_inventory", + "maximum_complexity": 12 + }, + { + "function": "scripts/local-social-fixture.py:directory_digest", + "maximum_complexity": 16 + }, + { + "function": "scripts/local-social-fixture.py:exact_persona_test_node", + "maximum_complexity": 11 + }, + { + "function": "scripts/local-social-fixture.py:load_exported_persona_attachments", + "maximum_complexity": 31 + }, + { + "function": "scripts/local-social-fixture.py:matches", + "maximum_complexity": 22 + }, + { + "function": "scripts/local-social-fixture.py:photo_attempt_matches", + "maximum_complexity": 12 + }, + { + "function": "scripts/local-social-fixture.py:reconstruct_persona_result_v2", + "maximum_complexity": 66 + }, + { + "function": "scripts/local-social-fixture.py:simulator_metadata", + "maximum_complexity": 23 + }, + { + "function": "scripts/local-social-fixture.py:valid_blossom_authorization", + "maximum_complexity": 29 + }, + { + "function": "scripts/local-social-fixture.py:valid_bud11_server_domain", + "maximum_complexity": 12 + }, + { + "function": "scripts/local-social-fixture.py:valid_nostr_event", + "maximum_complexity": 16 + }, + { + "function": "scripts/local-social-fixture.py:validate_persona_attempt_evidence", + "maximum_complexity": 59 + }, + { + "function": "scripts/local-social-fixture.py:validate_persona_evidence", + "maximum_complexity": 31 + }, + { + "function": "scripts/local-social-fixture.py:validate_persona_result", + "maximum_complexity": 43 + }, + { + "function": "scripts/local-social-fixture.py:validate_persona_suite", + "maximum_complexity": 28 + }, + { + "function": "scripts/local-social-fixture.py:verify_bip340", + "maximum_complexity": 11 + }, + { + "function": "scripts/local-social-fixture.py:verify_persona", + "maximum_complexity": 18 + }, + { + "function": "scripts/test_local_social_fixture.py:mutate_bud11_event", + "maximum_complexity": 29 + } ], "bounded_module": [ "Tera/Runtime/TeraUserMessageClassifier.swift", "Tera/Runtime/TeraUserMessages.swift", + "Tera/Views/TeraSupportSettingsSection.swift", "scripts/maintainability_ratchet.py", "scripts/package_contract.py", - "scripts/test_package_contract.py" + "scripts/package_privacy.py", + "scripts/test_package_contract.py", + "scripts/test_package_privacy.py" ] }