fix: revert E2E AEAD wrapping (broke multi-client voice); add Android CAMERA
Voice regression: EncryptingTransport encrypts media with the pairwise client↔relay session key, but the relay forwards bytes without re-encrypting per recipient. Sender's key_A ≠ recipient's key_B → recipient cannot decrypt → silent audio between mac and android. Drop the wrapper; restore plaintext- over-QUIC-TLS to the relay. Proper E2E needs MLS group keys or relay hop-by- hop re-encryption (future PRD). Android camera: add CAMERA manifest permission + runtime request via MainActivity. NOTE: still not sufficient — Tauri/Wry's WebChromeClient does not grant getUserMedia, so video on Android needs a Tauri plugin override or native Camera2 path. Documented in MainActivity.kt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,9 @@
|
|||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-feature android:name="android.hardware.microphone" android:required="true" />
|
<uses-feature android:name="android.hardware.microphone" android:required="true" />
|
||||||
|
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||||
|
|
||||||
<!-- AndroidTV support -->
|
<!-- AndroidTV support -->
|
||||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||||
|
|||||||
@@ -16,10 +16,19 @@ class MainActivity : TauriActivity() {
|
|||||||
private const val AUDIO_PERMISSIONS_REQUEST = 4242
|
private const val AUDIO_PERMISSIONS_REQUEST = 4242
|
||||||
private val REQUIRED_AUDIO_PERMISSIONS = arrayOf(
|
private val REQUIRED_AUDIO_PERMISSIONS = arrayOf(
|
||||||
Manifest.permission.RECORD_AUDIO,
|
Manifest.permission.RECORD_AUDIO,
|
||||||
Manifest.permission.MODIFY_AUDIO_SETTINGS
|
Manifest.permission.MODIFY_AUDIO_SETTINGS,
|
||||||
|
Manifest.permission.CAMERA
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: granting CAMERA at the Android system layer is necessary but NOT
|
||||||
|
// sufficient for video on Android. Tauri/Wry's internal WebChromeClient
|
||||||
|
// does not currently grant `getUserMedia` permission requests, so the
|
||||||
|
// browser-layer getUserMedia call still fails even after the OS grants
|
||||||
|
// CAMERA. Fixing this needs either a Tauri plugin that overrides the
|
||||||
|
// WebChromeClient, or a native Camera2/CameraX capture path that bypasses
|
||||||
|
// the WebView. Tracked as a follow-up.
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|||||||
@@ -615,10 +615,12 @@ impl CallEngine {
|
|||||||
video_codec = ?hs.video_codec,
|
video_codec = ?hs.video_codec,
|
||||||
"first-join diag: connected to relay, handshake complete"
|
"first-join diag: connected to relay, handshake complete"
|
||||||
);
|
);
|
||||||
Arc::new(wzp_client::encrypted_transport::EncryptingTransport::new(
|
// NOTE: see comment in CallEngine::start (~line 1585) — we intentionally
|
||||||
transport,
|
// do NOT wrap with EncryptingTransport. The pairwise client↔relay session
|
||||||
hs.session,
|
// key can't be used end-to-end without MLS or relay re-encryption.
|
||||||
))
|
drop(hs.session);
|
||||||
|
let _ = hs.video_codec;
|
||||||
|
transport
|
||||||
} else {
|
} else {
|
||||||
info!(
|
info!(
|
||||||
t_ms = call_t0.elapsed().as_millis(),
|
t_ms = call_t0.elapsed().as_millis(),
|
||||||
@@ -1584,6 +1586,12 @@ impl CallEngine {
|
|||||||
// accept_handshake handler. See the android branch's
|
// accept_handshake handler. See the android branch's
|
||||||
// comment for the full rationale.
|
// comment for the full rationale.
|
||||||
let quinn_transport = transport.clone();
|
let quinn_transport = transport.clone();
|
||||||
|
// NOTE: EncryptingTransport is intentionally NOT wrapping the transport here.
|
||||||
|
// The client↔relay handshake derives a pairwise session key, but the relay
|
||||||
|
// forwards media without decrypt+re-encrypt — so a recipient with a different
|
||||||
|
// pairwise key cannot decrypt the sender's ciphertext. True E2E for the SFU
|
||||||
|
// model needs MLS group keys (or hop-by-hop relay re-encryption); until that
|
||||||
|
// PRD lands, media goes plaintext-over-QUIC-TLS to the relay.
|
||||||
let (_negotiated_video_codec, transport): (_, Arc<dyn wzp_proto::MediaTransport>) =
|
let (_negotiated_video_codec, transport): (_, Arc<dyn wzp_proto::MediaTransport>) =
|
||||||
if !is_direct_p2p {
|
if !is_direct_p2p {
|
||||||
let hs =
|
let hs =
|
||||||
@@ -1594,13 +1602,8 @@ impl CallEngine {
|
|||||||
e
|
e
|
||||||
})?;
|
})?;
|
||||||
info!(video_codec = ?hs.video_codec, "handshake complete");
|
info!(video_codec = ?hs.video_codec, "handshake complete");
|
||||||
let enc = Arc::new(
|
drop(hs.session);
|
||||||
wzp_client::encrypted_transport::EncryptingTransport::new(
|
(hs.video_codec, transport)
|
||||||
transport,
|
|
||||||
hs.session,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
(hs.video_codec, enc)
|
|
||||||
} else {
|
} else {
|
||||||
info!("direct P2P — skipping relay handshake (QUIC TLS is the encryption layer)");
|
info!("direct P2P — skipping relay handshake (QUIC TLS is the encryption layer)");
|
||||||
(None, transport)
|
(None, transport)
|
||||||
|
|||||||
Reference in New Issue
Block a user