Service: fix divergences from the main app's send path
Local checkpoint - NOT for public release. Audited ServiceSendHost against MainForm's send path (Ed: make the service reuse the same code, be just as stable). Three real divergences found and fixed: 1. ENCRYPTION FINGERPRINT (critical): the host set sender.AudioKey but NOT sender.AudioFingerprint. The main app (RecomputeAudioCrypto) sets both, and the peer verifies the fingerprint before accepting a stream - so the service's encrypted audio would have been REJECTED at the far end. Now derives and sets both from the password. 2. OPUS FRAME: the main app applies EffectiveOpusFrameSamples (the "Small" send rate halves the Opus frame); the host passed the raw frame, so it would encode differently than the main app for the same profile. Now reuses MainForm.EffectiveOpusFrameSamples (made internal - same code, not a copy). 3. PEER PORT: send target fell back to the profile's LOCAL AudioPort; the correct default is RemPacket.DefaultPeerDialPort (what the main app's manual-peer path uses). Same value today but the right constant. Reviewed and OK: sender defaults to WasapiOnly (no SetAudioMode needed); BuildSendSpecs matches ApplySendSources for explicit-device profiles; default-device changes are covered by the device-change watcher; direct-send (no relay/StartReceiving) is the intended v1 scope. New self-test "Service sender parity" asserts key + fingerprint + effective Opus frame match the main app. Gate 26/26. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4fb490b2d5
commit
78a9aa6572
@@ -63,6 +63,7 @@ internal static class SelfTest
|
||||
RunStep(results, "Per-application capture lifecycle", AppSendCaptureLifecycle);
|
||||
RunStep(results, "Lifecycle churn (modes, sources, pan/EQ, send/receive)", LifecycleChurn);
|
||||
RunStep(results, "Service app-yield token", ServiceInteractivePresence);
|
||||
RunStep(results, "Service sender parity (crypto + Opus frame)", ServiceSenderParity);
|
||||
RunStep(results, "Service send host (headless stream + yield)", ServiceSendHostStream);
|
||||
RunStep(results, "Service registration args", ServiceRegistrationArgs);
|
||||
RunStep(results, "Recording engine (all formats + source gate + mono)", RecordingEngine);
|
||||
@@ -677,6 +678,39 @@ internal static class SelfTest
|
||||
return "sc create args quoted correctly for a spaced path";
|
||||
}
|
||||
|
||||
/// <summary>The service must configure the sender EXACTLY like the main app: derive both the audio key
|
||||
/// AND the fingerprint from the password (a missing fingerprint gets the encrypted stream rejected at
|
||||
/// the peer), and apply the send-rate-adjusted Opus frame (the "Small" rate halves it). Guards the
|
||||
/// divergences found auditing the service against the main app.</summary>
|
||||
private static string? ServiceSenderParity()
|
||||
{
|
||||
var profile = new Profile
|
||||
{
|
||||
Title = "parity",
|
||||
Codec = AudioTransportCodec.Opus,
|
||||
OpusFrameSamplesPerChannel = 960, // broadcast
|
||||
SendRate = SendRate.Tight, // "Small" — should halve the Opus frame to 480
|
||||
WasapiSendMode = "devices",
|
||||
};
|
||||
profile.SelectedWasapiSendOutputs.Add("fake-device-id"); // a source so ApplyProfile proceeds
|
||||
profile.SelectedConnectedPeers.Add("127.0.0.1:47999");
|
||||
const string pw = "hunter2";
|
||||
profile.Password = RemSoundCrypto.Obfuscate(pw);
|
||||
|
||||
using var host = new ServiceSendHost(() => profile);
|
||||
Check(host.ApplyProfile(profile), "ApplyProfile should proceed with a source + peer + password");
|
||||
var cfg = host.SenderConfigForTest;
|
||||
|
||||
Check(cfg.Key is { Length: > 0 } && cfg.Key.SequenceEqual(RemSoundCrypto.DeriveKey(pw)),
|
||||
"the service must set the audio key = DeriveKey(password)");
|
||||
Check(cfg.Fingerprint is { Length: > 0 } && cfg.Fingerprint.SequenceEqual(RemSoundCrypto.Fingerprint(pw)),
|
||||
"the service must set the audio FINGERPRINT = Fingerprint(password), or the peer rejects the stream");
|
||||
Check(cfg.Codec == AudioTransportCodec.Opus, "codec must round-trip to the sender");
|
||||
Check(cfg.Frame == MainForm.EffectiveOpusFrameSamples(AudioTransportCodec.Opus, 960, SendRate.Tight) && cfg.Frame == 480,
|
||||
$"the Small send rate must halve the Opus frame like the main app (got {cfg.Frame})");
|
||||
return "key + fingerprint + effective Opus frame match the main app";
|
||||
}
|
||||
|
||||
/// <summary>The lock-screen service's app-yield token: while a hold is active the service must see an
|
||||
/// interactive app present; once released (or on crash — the OS frees the mutex) it must see none.
|
||||
/// Uses a unique token name so the test is immune to a real RemSound running alongside the gate.</summary>
|
||||
|
||||
Reference in New Issue
Block a user