iOS↔Android parity: Keychain vault, requestPath, RNS id export/import, voice clips#45
Merged
Merged
Conversation
…t/import Bring iOS to parity with Android on the remaining tracks. Identity vault (closes deferred half of audit 2026-05-13 HIGH-1): - New KeychainIdentityVault seals the long-term private keys with AES-256-CBC + HMAC-SHA256 (reusing IosCryptoProvider) under a 32-byte master key kept in the iOS Keychain via a new rcr_keychain_get_or_create_key Swift-bridge fn (kSecAttrAccessibleWhenUnlockedThisDeviceOnly, never syncs off-device). Replaces the pass-through PlaintextIdentityVault. - IosIdentityRepo.save seals with a degrade-to-plaintext fallback if the Keychain is unavailable (mirrors the Android Keystore-unavailable path); load() migrates pre-Keychain installs in place (raw 32-byte key vs 97-byte sealed blob disambiguates). - iosTest round-trip + tamper/wrong-key/truncation suite. requestPath: iOS now fires an RNS path request when following a cross-node Nomad link to an unseen hash (NomadView/ConversationView), via requestPathBridge — matches Android resolveOrPrepareDestination. RNS-format identity export/import: Settings gains the unencrypted cross-tool RNS export (behind a warning) and auto-detects a 64-byte RNS file on import, alongside the existing .rmid flow. Also: refresh stale IosCryptoProvider kdoc (Curve25519 is implemented via the CryptoKit bridge, not throwing) and record the parity sweep in docs/ROADMAP.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Container half of iOS voice-clip support. Kept in memory-safe Kotlin (not C) on purpose: demux() runs on attacker-controlled bytes, so only the bounded individual Opus packets ever reach the libopus C decoder — the Ogg page/lacing/CRC parsing stays out of the untrusted-input C path. - demux(): hostile-input-hardened — every length bounds-checked against the buffer, no allocation from unchecked fields, throws on malformation. - mux(): RFC 7845 stream (OpusHead + OpusTags + one-packet-per-page audio) playable by Android MediaPlayer / Sideband. - Ogg CRC-32 (poly 0x04C11DB7, no reflection) cross-checked in tests against an independent bitwise implementation — the #1 interop gotcha. commonTest suite runs under iosSimulatorArm64Test + the Android test job. Codec (libopus) integration is the next, separately-reviewed step. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Inline FIELD_AUDIO (Opus-in-Ogg) record + playback on iOS, the last iOS↔Android parity gap. AVFoundation can't touch Opus, so the codec is libopus; the container is memory-safe Kotlin. DEPENDENCY + BUILD-PIPELINE CHANGE — review line-by-line before merge (per CLAUDE.md), do not auto-merge: - third_party/opus: pinned submodule @ v1.5.2 (ddbe4838). - shared/iosOpus/build.sh: CMake cross-compile to per-target libopus.a. - opus cinterop + per-target -L + buildIosOpus wiring in build.gradle.kts. - ios-build / ios-app-build / ios-release: submodules: recursive on checkout. Implementation: - codec/OggOpus.kt already landed (RFC 7845 mux/demux, unit-tested). Only bounded Opus packets reach C; Ogg parsing stays out of the untrusted-input C path. No libogg. - iosMain OpusCodec.kt: PCM↔Ogg encode/decode; decode is size- and decoded-sample-capped (decompression-bomb guard); Swift bridges. - Swift VoiceAudio.swift: AVAudioEngine recorder (48k mono Int16) + player; composer mic button; MessageBubble play/stop bubble (Codec2 shown as unsupported). NSMicrophoneUsageDescription added. - iOS schema: audioMode column + migration 8.sqm + IosMessageRepo wiring, so inbound clips render as voice not generic files (they already degraded to a saveable .ogg attachment without this). Security: decode only reached for stored (verified-on-receive) rows; caps before libopus; libopus is heavily fuzzed (WebRTC/OSS-Fuzz). UNVERIFIED on WSL — needs a CI run (libopus CMake build + iosSimulatorArm64Test for OggOpusTest) and on-device record/playback + Sideband interop. Not run yet per the rationed-CI constraint. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ios-app-build failed at the Ld step — "library 'opus' not found", undefined _opus_* symbols. The Shared framework auto-links -lopus, so the app's Xcode link needs the per-slice libopus.a on LIBRARY_SEARCH_PATHS, exactly like the existing iosCryptoBridge entries. (ios-build / the XCFramework test binary already linked it via the gradle per-target -L, which is why that run was green.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings iOS to parity with Android across the remaining tracks. Both iOS CI
workflows are green on this branch (
ios-buildincl.iosSimulatorArm64Test,and
ios-app-build).submodule, CMake build script, cinterop, and
submodules: recursiveon theiOS workflows). Per CLAUDE.md this is the highest-scrutiny category — please
review it line by line and do not fast-merge.
and the framing/CRC unit tests pass, but actual mic capture, playback, and
Opus interop with a real Android/Sideband peer need an on-device test.
What's in here
Identity vault (security — closes deferred half of audit 2026-05-13 HIGH-1)
KeychainIdentityVault: seals identity private keys with AES-256-CBC +HMAC-SHA256 under a 32-byte master key in the iOS Keychain
(
kSecAttrAccessibleWhenUnlockedThisDeviceOnly, never syncs off-device),replacing the pass-through
PlaintextIdentityVault.rcr_keychain_get_or_create_keySwift-bridge fn.IosIdentityReposeals with a degrade-to-plaintext fallback;load()migrates pre-Keychain installs in place (32-byte raw key vs 97-byte sealed).
iosTestround-trip + tamper/wrong-key suite (passing in CI).Engine wiring
requestPathfired on cross-node Nomad link follow (matches Android'sresolveOrPrepareDestination).64-byte auto-detect on import) alongside the existing
.rmidflow.Voice clips (
FIELD_AUDIO/ Opus-in-Ogg)third_party/opus@ v1.5.2),built from source via CMake per iOS slice. No libogg — the Ogg container
(RFC 7845) is memory-safe Kotlin (
OggOpus.kt, unit-tested, CRCcross-checked), so only bounded Opus packets reach the C decoder.
OpusCodec.kt: PCM↔Ogg encode/decode with decode-side size + sample caps(decompression-bomb guard).
VoiceAudio.swift(AVAudioEngine record/playback), composer micbutton, play/stop bubble (Codec2 shown as unsupported); mic usage string.
audioModecolumn + migration8.sqmso clips render as voice.Security posture
(verified-on-receive) rows. libopus is the WebRTC/OSS-Fuzz-hardened codec,
pinned and built from source (no build-time fetch).
Not addressed (platform-impossible on iOS, documented)
Bluetooth Classic SPP, USB-serial, ALN BLE tunnel.
🤖 Generated with Claude Code