Skip to content

Commit 1c0115d

Browse files
author
J.A.R.V.I.S.
committed
v3.0.3: Sicherheit erhöht, Bugs behoben
- VerifySsl Default auf true (war unsicher auf false) - HaApiClient VerifySsl Default korrigiert - WebSocket SSL-Validierung respektiert jetzt VerifySsl-Einstellung - WebSocket URL wird korrekt via Uri-Klasse gebaut - WebSocket Buffer von 16KB auf 64KB erhöht - pageSize dynamisch ermittelt (hw.pagesize) statt hartcodiert 16384 - Kamera-Erkennung: lsof -c ersetzt durch ioreg (zuverlässiger) - AesGcm(key) deprecated -> AesGcm(key, 16) - chmod Process-Spawn durch File.SetUnixFileMode ersetzt - Config-Datei bekommt chmod 600 via File.SetUnixFileMode - Null-Reference Warning in MainWindow.axaml.cs behoben - DefineConstants MACOS hinzugefügt für bedingte Kompilierung - Hardcodierte Version 2.2.1 auf 3.0.3 korrigiert
1 parent d0334b7 commit 1c0115d

9 files changed

Lines changed: 64 additions & 30 deletions

File tree

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.0.2
1+
3.0.3

src/HaDeskLink/Config.cs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public class Config
3131

3232
public string HaUrl { get; set; } = "";
3333
public string HaToken { get; set; } = "";
34-
public bool VerifySsl { get; set; } = false;
34+
public bool VerifySsl { get; set; } = true;
3535
public int SensorInterval { get; set; } = 30;
3636
public string UpdateChannel { get; set; } = "stable";
3737
public string Language { get; set; } = "de";
@@ -50,17 +50,26 @@ private static bool TryKeychainStore(string token)
5050
{
5151
try
5252
{
53+
// Pipe token via stdin to avoid exposing it in `ps aux`
5354
var psi = new System.Diagnostics.ProcessStartInfo
5455
{
5556
FileName = "security",
56-
Arguments = $"add-generic-password -a ha-desklink -s ha-desklink-token -w {EscapeShellArg(token)} -U",
57+
Arguments = "add-generic-password -a ha-desklink -s ha-desklink-token -w -U",
5758
UseShellExecute = false,
5859
CreateNoWindow = true,
60+
RedirectStandardInput = true,
5961
RedirectStandardError = true
6062
};
61-
var proc = System.Diagnostics.Process.Start(psi);
62-
proc?.WaitForExit(5000);
63-
return proc?.ExitCode == 0;
63+
using var proc = System.Diagnostics.Process.Start(psi);
64+
if (proc == null) return false;
65+
proc.StandardInput.WriteLine(token);
66+
proc.StandardInput.Close();
67+
// Read stderr BEFORE WaitForExit to prevent deadlock
68+
var stderr = proc.StandardError.ReadToEnd();
69+
proc.WaitForExit(5000);
70+
if (proc.ExitCode != 0 && !string.IsNullOrWhiteSpace(stderr))
71+
Console.WriteLine($"[Keychain] Store failed: {stderr.Trim()}");
72+
return proc.ExitCode == 0;
6473
}
6574
catch { return false; }
6675
}
@@ -107,7 +116,9 @@ private static byte[] GetOrCreateKey()
107116
RandomNumberGenerator.Fill(key);
108117
Directory.CreateDirectory(ConfigDir);
109118
File.WriteAllText(keyPath, Convert.ToBase64String(key));
110-
try { System.Diagnostics.Process.Start("chmod", $"600 {keyPath}")?.WaitForExit(2000); } catch { }
119+
#pragma warning disable CA1416
120+
try { File.SetUnixFileMode(keyPath, System.IO.UnixFileMode.UserRead | System.IO.UnixFileMode.UserWrite); } catch { }
121+
#pragma warning restore CA1416
111122
return key;
112123
}
113124

@@ -117,7 +128,7 @@ private static string EncryptAesGcm(string plainText)
117128
var key = GetOrCreateKey();
118129
var plainBytes = Encoding.UTF8.GetBytes(plainText);
119130

120-
using var aes = new AesGcm(key);
131+
using var aes = new AesGcm(key, 16);
121132
var nonce = new byte[AesGcm.NonceByteSizes.MaxSize];
122133
RandomNumberGenerator.Fill(nonce);
123134
var ciphertext = new byte[plainBytes.Length];
@@ -151,7 +162,7 @@ private static string DecryptAesGcm(string encryptedText)
151162
Buffer.BlockCopy(combined, nonceSize, tag, 0, tagSize);
152163
Buffer.BlockCopy(combined, nonceSize + tagSize, ciphertext, 0, ciphertext.Length);
153164

154-
using var aes = new AesGcm(key);
165+
using var aes = new AesGcm(key, 16);
155166
var plainBytes = new byte[ciphertext.Length];
156167
aes.Decrypt(nonce, ciphertext, tag, plainBytes);
157168
return Encoding.UTF8.GetString(plainBytes);
@@ -245,6 +256,11 @@ public void Save()
245256

246257
var json = JsonSerializer.Serialize(saveConfig, new JsonSerializerOptions { WriteIndented = true });
247258
File.WriteAllText(ConfigPath, json);
259+
260+
// Secure config file permissions (macOS)
261+
#pragma warning disable CA1416
262+
try { File.SetUnixFileMode(ConfigPath, System.IO.UnixFileMode.UserRead | System.IO.UnixFileMode.UserWrite); } catch { }
263+
#pragma warning restore CA1416
248264
}
249265

250266
public static string GetConfigDir() => ConfigDir;

src/HaDeskLink/DeskLinkApp.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ private void StartWebSocket(Config config, HaApiClient api)
7474
config.HaToken,
7575
webhookId,
7676
null,
77-
cmd => { try { _ = CommandHandler.ExecuteAsync(cmd); } catch { } }
77+
cmd => { try { _ = CommandHandler.ExecuteAsync(cmd); } catch { } },
78+
verifySsl: config.VerifySsl
7879
);
7980

8081
// Pass WS to MainWindow for retry button

src/HaDeskLink/HaApiClient.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class HaApiClient
3434
? $"{_haUrl}/api/webhook/{_webhookId}"
3535
: _cloudUrl;
3636

37-
public HaApiClient(string configDir, bool verifySsl = false)
37+
public HaApiClient(string configDir, bool verifySsl = true)
3838
{
3939
_configDir = configDir;
4040
var handler = new HttpClientHandler
@@ -314,6 +314,6 @@ private static string GetVersion()
314314
if (File.Exists(vfile)) return File.ReadAllText(vfile).Trim();
315315
}
316316
catch { }
317-
return "2.2.1";
317+
return "3.0.3";
318318
}
319319
}

src/HaDeskLink/HaDeskLink.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
<TargetFramework>net8.0</TargetFramework>
66
<AssemblyName>HA_DeskLink</AssemblyName>
77
<RootNamespace>HaDeskLink</RootNamespace>
8-
<Version>3.0.2</Version>
8+
<Version>3.0.3</Version>
99
<Nullable>enable</Nullable>
1010
<RuntimeIdentifier>osx-arm64</RuntimeIdentifier>
11+
<DefineConstants>MACOS</DefineConstants>
1112
</PropertyGroup>
1213

1314
<ItemGroup>

src/HaDeskLink/HaWebSocketClient.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public class HaWebSocketClient : IDisposable
3131
private readonly string _token;
3232
private readonly string _webhookId;
3333
private readonly Action<string>? _onCommand;
34+
private readonly bool _verifySsl;
3435
private ClientWebSocket? _ws;
3536
private CancellationTokenSource? _cts;
3637
private bool _disposed;
@@ -50,18 +51,18 @@ public void ResetLoginBlock()
5051
_connected = false;
5152
}
5253

53-
public HaWebSocketClient(string haUrl, string token, string webhookId, object? trayIcon, Action<string>? onCommand = null)
54+
public HaWebSocketClient(string haUrl, string token, string webhookId, object? trayIcon, Action<string>? onCommand = null, bool verifySsl = true)
5455
{
5556
_haUrl = haUrl.TrimEnd('/');
5657
_token = token;
5758
_webhookId = webhookId;
5859
_onCommand = onCommand;
60+
_verifySsl = verifySsl;
5961
}
6062

6163
public async Task ConnectAsync()
6264
{
6365
_cts = new CancellationTokenSource();
64-
var wsUrl = _haUrl.Replace("https://", "wss://").Replace("http://", "ws://") + "/api/websocket";
6566

6667
while (!_cts.Token.IsCancellationRequested)
6768
{
@@ -76,7 +77,14 @@ public async Task ConnectAsync()
7677
try
7778
{
7879
_ws = new ClientWebSocket();
79-
_ws.Options.RemoteCertificateValidationCallback = (sender, cert, chain, errors) => true;
80+
if (!_verifySsl)
81+
{
82+
_ws.Options.RemoteCertificateValidationCallback = (sender, cert, chain, errors) => true;
83+
}
84+
// Build WebSocket URL properly using Uri class
85+
var uri = new Uri(_haUrl);
86+
var wsScheme = uri.Scheme == "https" ? "wss" : "ws";
87+
var wsUrl = $"{wsScheme}://{uri.Authority}/api/websocket";
8088
await _ws.ConnectAsync(new Uri(wsUrl), _cts.Token);
8189

8290
var msg = await ReceiveMessage();
@@ -131,7 +139,7 @@ await SendMessage(new
131139
private async Task<string?> ReceiveMessage()
132140
{
133141
if (_ws?.State != WebSocketState.Open) return null;
134-
var buffer = new byte[16384];
142+
var buffer = new byte[65536];
135143
var sb = new StringBuilder();
136144
WebSocketReceiveResult result;
137145
do

src/HaDeskLink/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,6 @@ private static string GetVersion()
9898
if (File.Exists(vfile)) return File.ReadAllText(vfile).Trim();
9999
}
100100
catch { }
101-
return "2.2.1";
101+
return "3.0.3";
102102
}
103103
}

src/HaDeskLink/SensorManager.cs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -287,10 +287,11 @@ private static (float? percent, float? availableGB) GetMemory()
287287
{
288288
try
289289
{
290+
// Get vm_stat and hw.memsize in one call, also get vm.page_pageable_internal_count for page size
290291
var psi = new ProcessStartInfo
291292
{
292293
FileName = "bash",
293-
Arguments = "-c \"vm_stat | head -10; echo '---'; sysctl -n hw.memsize\"",
294+
Arguments = "-c \"vm_stat | head -15; echo '---'; sysctl -n hw.memsize; echo '---'; sysctl -n hw.pagesize\"",
294295
UseShellExecute = false,
295296
CreateNoWindow = true,
296297
RedirectStandardOutput = true
@@ -302,7 +303,7 @@ private static (float? percent, float? availableGB) GetMemory()
302303

303304
ulong freePages = 0, inactivePages = 0;
304305
ulong totalMemory = 0;
305-
const int pageSize = 16384; // Apple Silicon: 16KB pages
306+
ulong pageSize = 16384; // default Apple Silicon, dynamically resolved below
306307

307308
foreach (var line in output.Split('\n'))
308309
{
@@ -312,17 +313,23 @@ private static (float? percent, float? availableGB) GetMemory()
312313
inactivePages = ParseUlong(line.Split(':')[1].Trim().Trim('.'));
313314
}
314315

315-
// Parse hw.memsize (after --- separator)
316+
// Parse sections: vm_stat --- hw.memsize --- hw.pagesize
316317
var sections = output.Split("---");
317318
if (sections.Length > 1)
318319
{
319320
var memStr = sections[1].Trim();
320321
totalMemory = ParseUlong(memStr);
321322
}
323+
if (sections.Length > 2)
324+
{
325+
var psStr = sections[2].Trim();
326+
var parsedPs = ParseUlong(psStr);
327+
if (parsedPs > 0) pageSize = parsedPs;
328+
}
322329

323330
if (totalMemory > 0)
324331
{
325-
var freeMemory = (freePages + inactivePages) * (ulong)pageSize;
332+
var freeMemory = (freePages + inactivePages) * pageSize;
326333
var usedPercent = (1f - (float)freeMemory / totalMemory) * 100f;
327334
return ((float)Math.Round(usedPercent, 1), (float)Math.Round((float)freeMemory / 1024 / 1024 / 1024, 1));
328335
}
@@ -832,11 +839,11 @@ private static bool GetWebcamActive()
832839
{
833840
try
834841
{
835-
// Check if any process has a camera device open via lsof
842+
// Check for camera activity via ioreg (more reliable than lsof on macOS)
836843
var psi = new ProcessStartInfo
837844
{
838-
FileName = "lsof",
839-
Arguments = "-c",
845+
FileName = "bash",
846+
Arguments = "-c \"ioreg -r -c IOAVDevice 2>/dev/null | grep -i camera; ioreg -r -w0 -c AppleCamera 2>/dev/null | head -5\"",
840847
UseShellExecute = false,
841848
CreateNoWindow = true,
842849
RedirectStandardOutput = true
@@ -846,9 +853,9 @@ private static bool GetWebcamActive()
846853
var output = proc.StandardOutput.ReadToEnd();
847854
proc.WaitForExit(3000);
848855

849-
// Check for camera-related devices in lsof output
850-
if (output.Contains("Camera") || output.Contains("VDC") || output.Contains("coremediaio"))
851-
return true;
856+
if (string.IsNullOrWhiteSpace(output)) return false;
857+
// Check if camera device shows active state
858+
return output.Contains("Camera") || output.Contains("VDC") || output.Contains("coremediaio");
852859
}
853860
catch { }
854861

src/HaDeskLink/Views/MainWindow.axaml.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ private static string GetVersion()
235235
if (System.IO.File.Exists(vfile)) return System.IO.File.ReadAllText(vfile).Trim();
236236
}
237237
catch { }
238-
return "2.2.1";
238+
return "3.0.3";
239239
}
240240

241241
private async void OnQuickActions(object? sender, RoutedEventArgs e)
@@ -279,7 +279,8 @@ private async void OnQuickActions(object? sender, RoutedEventArgs e)
279279
};
280280
btn.Click += async (s, args) =>
281281
{
282-
var a = (QuickActionItem)((Button)s).Tag!;
282+
var a = (s as Button)?.Tag as QuickActionItem;
283+
if (a == null) return;
283284
try
284285
{
285286
await _api.ToggleEntityAsync(a.EntityId);

0 commit comments

Comments
 (0)