Audit Apple mobile integration (#58)
This commit is contained in:
31
apple/Resources/App/PrivacyInfo.xcprivacy
Normal file
31
apple/Resources/App/PrivacyInfo.xcprivacy
Normal file
@@ -0,0 +1,31 @@
|
||||
<?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>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>35F9.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
31
apple/Resources/AutoFill/PrivacyInfo.xcprivacy
Normal file
31
apple/Resources/AutoFill/PrivacyInfo.xcprivacy
Normal file
@@ -0,0 +1,31 @@
|
||||
<?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>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>35F9.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
14
apple/Resources/Watch/PrivacyInfo.xcprivacy
Normal file
14
apple/Resources/Watch/PrivacyInfo.xcprivacy
Normal 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>
|
||||
89
apple/TESTING.md
Normal file
89
apple/TESTING.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Apple mobile integration audit
|
||||
|
||||
This is the source-of-truth coverage and release-test checklist for the iPhone
|
||||
and Apple Watch milestone. Automated evidence is required on every change; a
|
||||
manual result is evidence only when the named device or simulator flow was
|
||||
actually exercised.
|
||||
|
||||
## Coverage matrix
|
||||
|
||||
| Area | Implemented boundary | Automated evidence | Manual release evidence |
|
||||
| --- | --- | --- | --- |
|
||||
| Home tab, remote activity, pull-to-refresh | UIKit renders Rust `MobileHomePage` and typed Commit/Fetch/Pull/Push actions | `mobile_home` unit tests, `git_embedded`, and `crates/apple` bridge tests | Exercise refresh and all four actions against a disposable HTTPS remote, including expired application-token recovery |
|
||||
| Passwords tab, navigation, view, and edit | Rust supplies typed paths, fields, drafts, revisions, and mutations; UIKit presents them | `mobile_passwords`, `entry_documents`, `mobile_mutation`, and `mobile_authentication` tests | Open nested entries, edit/save/cancel, and confirm stale-edit recovery |
|
||||
| Swipe move/copy/delete | Rust plans destinations, collisions, dirty-editor handling, and commits; UIKit also exposes accessibility actions | `plans_destinations_collisions_hidden_paths_and_stale_revisions` and authentication serialization tests | Exercise swipe and VoiceOver alternatives; confirm destructive prompts |
|
||||
| TOTP tab and sharing | Rust discovers, caches, parses, selects, and generates OTP data | `mobile_totp`, `otp`, and `mobile_watch` integration tests | Discover entries, read a code, change Watch selection, and verify countdown |
|
||||
| Preferences and biometric unlock | Rust owns preference/authentication state; LocalAuthentication and Keychain remain isolated Apple adapters | `authentication_leases`, `secret_store`, and mobile authentication tests | Enroll once, then test match, cancel, rejection, and changed enrollment using the documented sequence below |
|
||||
| GPG key QR transfer | Rust frames, validates, reconstructs, authenticates, and atomically stores keys; UIKit only scans/displays frames | `mobile_key_transfer` unit tests and `crypto_compatibility` | Test public/private transfer, interruption, duplicates, and capture shielding |
|
||||
| Watch synchronization | Rust owns selected snapshots, pairing identity, revisions, replay rejection, receipts, and revocation | `replacement_snapshots_reject_replays_conflicts_and_pairing_changes` | On a paired device, verify background delivery, replacement, replay rejection, revoke, and pairing change |
|
||||
| Watch display | Rust owns OTP calculation, timing, and ready/empty/syncing/stale/locked/error presentation; SwiftUI renders it | `watch_runtime_generates_view_ready_totp_and_clears_secrets_when_locked` plus ARM64 Watch target build | Test offline and stale-snapshot behavior, wrist-down privacy, VoiceOver, Dynamic Type, and supported Watch sizes |
|
||||
| Lifecycle and disclosure controls | Rust drops protected state; UIKit/SwiftUI clear views, time-limit pasteboard values, and shield private QR/captured content | authentication, presentation, TOTP, and Watch lifecycle tests; source audit in `apple_mobile_audit` | Background/foreground, lock, screenshot/capture, pasteboard expiry, and protected-data loss |
|
||||
| iPhone layouts and input | Native UIKit navigation, tables, alerts, refresh controls, semantic text styles, and hardware-keyboard behavior | iPhone simulator compile for the supported target | Exercise supported iPhone sizes, largest Dynamic Type, VoiceOver, contrast, and hardware keyboard |
|
||||
|
||||
The following are intentionally outside this milestone and must not be stubbed
|
||||
into the Apple apps: full CLI/TUI parity, command mode or command palette, SSH
|
||||
Git, non-TOTP Watch features, and production AutoFill behavior.
|
||||
|
||||
## Security boundary audit
|
||||
|
||||
- `crates/storage` owns repository access, parsing, Git, OTP, authentication,
|
||||
snapshot transfer, conflict handling, and secret policy.
|
||||
- The Apple Rust crates are mechanical UniFFI projections. Swift may collect
|
||||
input and call those APIs; it does not open a password-store repository,
|
||||
calculate OTP, parse snapshots, or launch processes.
|
||||
- Git URLs are rejected unless they use HTTPS before transport work. The Watch
|
||||
stores only the selected opaque snapshot in a passcode-protected, device-only
|
||||
Keychain item.
|
||||
- Apple production sources contain no logging calls. Secret values use typed
|
||||
redaction/zeroization in Rust; pasteboard values are removed after their Rust
|
||||
deadline; private QR and authenticated content are hidden during capture or
|
||||
lifecycle loss.
|
||||
- The iPhone and AutoFill privacy manifests declare in-container file metadata
|
||||
access (`C617.1`) and elapsed-time/timer use (`35F9.1`). The Watch manifest
|
||||
declares no required-reason API because its minimal Rust core does not open
|
||||
files or use a monotonic timer. No target declares tracking or developer data
|
||||
collection.
|
||||
- Face ID and camera access have purpose strings in `project.yml`. No custom
|
||||
entitlement is currently required: default signing supplies each target's
|
||||
application identifier and Keychain group, WatchConnectivity needs no added
|
||||
capability, and no App Group is used. Alternative-distribution entitlements
|
||||
belong to the final distribution issue.
|
||||
|
||||
`cargo test -p ironstorage --test apple_mobile_audit` enforces the source and privacy-manifest
|
||||
parts of this review so those boundaries cannot silently regress.
|
||||
|
||||
## Required automated gates
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
cargo fmt --all -- --check
|
||||
RUSTFLAGS="-D warnings" cargo check --workspace --all-targets
|
||||
cargo clippy --workspace --all-targets -- -D warnings
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
For Apple changes, regenerate the project and compile the exact affected iPhone
|
||||
and ARM64 Watch simulator targets. Confirm each built app bundle contains its
|
||||
own `PrivacyInfo.xcprivacy`.
|
||||
|
||||
## Manual biometric sequence
|
||||
|
||||
Keep the existing booted simulator, app container, bundle identifier, and Face
|
||||
ID enrollment. For first setup only, choose **Features > Face ID > Enrolled**,
|
||||
enable Biometric Unlock in IronStorage, unlock one entry, and enter the GPG
|
||||
passphrase once. Do not toggle enrollment again during normal validation.
|
||||
|
||||
For each normal check, tap the entry's **Unlock** control first and then choose
|
||||
**Features > Face ID > Matching Face**. Verify cancellation and rejection
|
||||
without changing enrollment. A changed-enrollment check is destructive because
|
||||
`biometryCurrentSet` invalidates the protected Keychain item; run it only when
|
||||
the tester is ready to enter the passphrase again.
|
||||
|
||||
## Manual evidence still required
|
||||
|
||||
Record the device model/OS, build commit, and pass/fail result for each manual
|
||||
row above. Physical paired-Watch deployment and WatchConnectivity testing are
|
||||
currently skipped by project direction, so issues closed without that evidence
|
||||
must carry the `untested` label. Do not describe a simulator build or an opened
|
||||
Xcode project as device validation.
|
||||
@@ -29,6 +29,7 @@ targets:
|
||||
- UIInterfaceOrientationPortrait
|
||||
sources:
|
||||
- Assets.xcassets
|
||||
- Resources/App/PrivacyInfo.xcprivacy
|
||||
- Sources/App
|
||||
- Generated/ironstorage_apple.swift
|
||||
dependencies:
|
||||
@@ -60,6 +61,7 @@ targets:
|
||||
NSExtensionPointIdentifier: com.apple.authentication-services-credential-provider-ui
|
||||
NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).CredentialProviderViewController
|
||||
sources:
|
||||
- Resources/AutoFill/PrivacyInfo.xcprivacy
|
||||
- Sources/AutoFill
|
||||
- Generated/ironstorage_apple.swift
|
||||
preBuildScripts:
|
||||
@@ -89,6 +91,7 @@ targets:
|
||||
WKCompanionAppBundleIdentifier: de.rfc1437.ironstorage
|
||||
sources:
|
||||
- Assets.xcassets
|
||||
- Resources/Watch/PrivacyInfo.xcprivacy
|
||||
- Sources/Watch
|
||||
- Generated/ironstorage_watch.swift
|
||||
preBuildScripts:
|
||||
|
||||
93
crates/storage/tests/apple_mobile_audit.rs
Normal file
93
crates/storage/tests/apple_mobile_audit.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
fn workspace_file(path: &str) -> String {
|
||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.and_then(std::path::Path::parent)
|
||||
.expect("workspace root")
|
||||
.to_path_buf();
|
||||
fs::read_to_string(root.join(path)).unwrap_or_else(|error| panic!("read {path}: {error}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apple_sources_preserve_the_mobile_security_boundary() {
|
||||
let app = workspace_file("apple/Sources/App/IronStorageApp.swift");
|
||||
let watch = workspace_file("apple/Sources/Watch/IronStorageWatchApp.swift");
|
||||
let autofill = workspace_file("apple/Sources/AutoFill/CredentialProviderViewController.swift");
|
||||
let bridges = format!(
|
||||
"{}\n{}",
|
||||
workspace_file("crates/apple/src/lib.rs"),
|
||||
workspace_file("crates/watch-apple/src/lib.rs")
|
||||
);
|
||||
|
||||
for (name, source) in [
|
||||
("iPhone", app.as_str()),
|
||||
("Watch", watch.as_str()),
|
||||
("AutoFill", autofill.as_str()),
|
||||
("Apple Rust bridges", bridges.as_str()),
|
||||
] {
|
||||
for forbidden in [
|
||||
"Process(",
|
||||
"NSTask",
|
||||
concat!("Command", "::new("),
|
||||
"posix_spawn(",
|
||||
] {
|
||||
assert!(!source.contains(forbidden), "{name} launches {forbidden}");
|
||||
}
|
||||
}
|
||||
|
||||
for (name, source) in [
|
||||
("iPhone", app.as_str()),
|
||||
("Watch", watch.as_str()),
|
||||
("AutoFill", autofill.as_str()),
|
||||
] {
|
||||
for forbidden in ["print(", "NSLog(", "Logger(", "os_log("] {
|
||||
assert!(
|
||||
!source.contains(forbidden),
|
||||
"{name} logs through {forbidden}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(!app.contains("FileManager.default"));
|
||||
for forbidden in ["otpauth://", "HMAC", "SHA1", "SHA256", "SHA512", ".gpg-id"] {
|
||||
assert!(
|
||||
!watch.contains(forbidden),
|
||||
"Watch implements storage or OTP domain behavior: {forbidden}"
|
||||
);
|
||||
}
|
||||
assert!(watch.contains("core.presentationAt"));
|
||||
assert!(watch.contains("SecItemCopyMatching"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_apple_bundle_has_an_audited_privacy_manifest() {
|
||||
let project = workspace_file("apple/project.yml");
|
||||
for path in [
|
||||
"Resources/App/PrivacyInfo.xcprivacy",
|
||||
"Resources/AutoFill/PrivacyInfo.xcprivacy",
|
||||
"Resources/Watch/PrivacyInfo.xcprivacy",
|
||||
] {
|
||||
assert!(project.contains(path), "Xcode target omits {path}");
|
||||
}
|
||||
|
||||
for path in [
|
||||
"apple/Resources/App/PrivacyInfo.xcprivacy",
|
||||
"apple/Resources/AutoFill/PrivacyInfo.xcprivacy",
|
||||
"apple/Resources/Watch/PrivacyInfo.xcprivacy",
|
||||
] {
|
||||
let manifest = workspace_file(path);
|
||||
assert!(manifest.contains("<key>NSPrivacyTracking</key>\n\t<false/>"));
|
||||
assert!(manifest.contains("<key>NSPrivacyCollectedDataTypes</key>\n\t<array/>"));
|
||||
}
|
||||
|
||||
let app = workspace_file("apple/Resources/App/PrivacyInfo.xcprivacy");
|
||||
assert!(app.contains("NSPrivacyAccessedAPICategoryFileTimestamp"));
|
||||
assert!(app.contains("C617.1"));
|
||||
assert!(app.contains("NSPrivacyAccessedAPICategorySystemBootTime"));
|
||||
assert!(app.contains("35F9.1"));
|
||||
assert!(project.contains("NSFaceIDUsageDescription"));
|
||||
assert!(project.contains("NSCameraUsageDescription"));
|
||||
}
|
||||
Reference in New Issue
Block a user