Skip to content

iOS↔Android parity: Keychain vault, requestPath, RNS id export/import, voice clips#45

Merged
thatSFguy merged 4 commits into
masterfrom
ios-parity-keychain-vault
Jun 28, 2026
Merged

iOS↔Android parity: Keychain vault, requestPath, RNS id export/import, voice clips#45
thatSFguy merged 4 commits into
masterfrom
ios-parity-keychain-vault

Conversation

@thatSFguy

Copy link
Copy Markdown
Owner

Brings iOS to parity with Android across the remaining tracks. Both iOS CI
workflows are green on this branch (ios-build incl. iosSimulatorArm64Test,
and ios-app-build).

⚠️ Review notes (read before merging)

  • This includes a native dependency + build-pipeline change (libopus
    submodule, CMake build script, cinterop, and submodules: recursive on the
    iOS workflows). Per CLAUDE.md this is the highest-scrutiny category — please
    review it line by line and do not fast-merge.
  • The voice-clip feature is not yet device-verified. CI proves it compiles
    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)

  • New 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.
  • Keychain key mgmt via a new rcr_keychain_get_or_create_key Swift-bridge fn.
  • IosIdentityRepo seals with a degrade-to-plaintext fallback; load()
    migrates pre-Keychain installs in place (32-byte raw key vs 97-byte sealed).
  • iosTest round-trip + tamper/wrong-key suite (passing in CI).

Engine wiring

  • requestPath fired on cross-node Nomad link follow (matches Android's
    resolveOrPrepareDestination).
  • RNS-format identity export/import in Settings (unencrypted, behind a warning;
    64-byte auto-detect on import) alongside the existing .rmid flow.

Voice clips (FIELD_AUDIO / Opus-in-Ogg)

  • libopus vendored as a pinned submodule (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, CRC
    cross-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).
  • Swift VoiceAudio.swift (AVAudioEngine record/playback), composer mic
    button, play/stop bubble (Codec2 shown as unsupported); mic usage string.
  • iOS schema: audioMode column + migration 8.sqm so clips render as voice.

Security posture

  • Only bounded Opus packets reach C; Ogg parsing stays in memory-safe Kotlin.
  • Decode is size/duration-capped and only reachable for stored
    (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

thatSFguy and others added 4 commits June 27, 2026 19:40
…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>
@thatSFguy
thatSFguy merged commit 952f540 into master Jun 28, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant