Skip to content

Commit

Permalink
Improve secure inclusion stability
Browse files Browse the repository at this point in the history
  • Loading branch information
jdomnitz committed Jun 20, 2023
1 parent 61026ae commit ede1e4a
Show file tree
Hide file tree
Showing 9 changed files with 331 additions and 172 deletions.
138 changes: 116 additions & 22 deletions TestConsole/TestConsole.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
using Serilog;
using ZWaveDotNet.Entities;
using System.Reflection;
using ZWaveDotNet.CommandClasses;
using ZWaveDotNet.Enums;
using ZWaveDotNet.Entities.Enums;
using System.Xml.Linq;

namespace ExampleConsole
{
public class TestConsole
{
private static readonly string? Version = Assembly.GetAssembly(typeof(Controller))!.GetName().Version?.ToString(3);
private static Controller? controller;
private static HashSet<ushort> InterviewList = new HashSet<ushort>();
private static HashSet<ushort> ReadyList = new HashSet<ushort>();
private static readonly HashSet<ushort> InterviewList = new HashSet<ushort>();
private static readonly HashSet<ushort> ReadyList = new HashSet<ushort>();
private static RFRegion region = RFRegion.Unknown;
private static LinkedList<string> Reports = new LinkedList<string>();
private enum Mode { Display, Inclusion, Exclusion};
private static Mode currentMode = Mode.Display;

static async Task Main()
static async Task Main(string[] args)
{
Log.Logger = new LoggerConfiguration().WriteTo.File("console.log").CreateLogger();

Expand All @@ -30,6 +36,8 @@ static async Task Main()
//Add event listeners before starting
controller.NodeInfoUpdated += Controller_NodeInfoUpdated;
controller.NodeReady += Controller_NodeReady;
controller.NodeExcluded += Controller_NodeExcluded;
controller.InclusionStopped += Controller_InclusionStopped;

//Start the controller interview
Console.WriteLine("Interviewing Controller...");
Expand All @@ -42,15 +50,86 @@ static async Task Main()
if (File.Exists("nodecache.db"))
await controller.ImportNodeDBAsync("nodecache.db");

await MainLoop();
_ = Task.Factory.StartNew(MainLoop);
await InputLoop();
}

private static void Controller_InclusionStopped(object? sender, EventArgs e)
{
currentMode = Mode.Display;
}

private static void Controller_NodeExcluded(object? sender, EventArgs e)
{
Node node = (Node)sender!;
InterviewList.Remove(node.ID);
ReadyList.Remove(node.ID);
currentMode = Mode.Display;
}

private static async Task InputLoop()
{
while (true)
{
ConsoleKeyInfo key = Console.ReadKey();
if (key.Key == ConsoleKey.E)
{
currentMode = Mode.Exclusion;
await controller!.StartExclusion();
PrintMain();
}
else if (key.Key == ConsoleKey.I)
{
currentMode = Mode.Inclusion;
await controller!.StartInclusion(InclusionStrategy.PreferS2, 12345);
PrintMain();
}
else if (key.Key == ConsoleKey.S)
{
if (currentMode == Mode.Exclusion)
await controller!.StopExclusion();
else
await controller!.StopInclusion();
currentMode = Mode.Display;
PrintMain();
}
}
}

private static async void Controller_NodeReady(object? sender, EventArgs e)
{
ushort id = ((Node)sender!).ID;
ReadyList.Add(id);
InterviewList.Add(id);
Node node = (Node)sender!;
InterviewList.Add(node.ID);
ReadyList.Add(node.ID);
await controller!.ExportNodeDBAsync("nodecache.db");
AttachListeners(node);
}

private static void AttachListeners(Node node)
{
if (node.HasCommandClass(CommandClass.SensorMultiLevel))
node.GetCommandClass<SensorMultiLevel>()!.Updated += Node_Updated;
if (node.HasCommandClass(CommandClass.Meter))
node.GetCommandClass<Meter>()!.Updated += Node_Updated;
if (node.HasCommandClass(CommandClass.Notification) && node.GetCommandClass<Notification>() is Notification not) //ZWave Weirdness
not.Updated += Node_Updated;
if (node.HasCommandClass(CommandClass.Battery))
node.GetCommandClass<Battery>()!.Status += Node_Updated;
if (node.HasCommandClass(CommandClass.SensorBinary))
node.GetCommandClass<SensorBinary>()!.Updated += Node_Updated;
if (node.HasCommandClass(CommandClass.SensorAlarm))
node.GetCommandClass<SensorAlarm>()!.Alarm += Node_Updated;
if (node.HasCommandClass(CommandClass.SwitchBinary))
node.GetCommandClass<SwitchBinary>()!.SwitchReport += Node_Updated;
}

private static async Task Node_Updated(Node sender, CommandClassEventArgs args)
{
if (args.Report == null)
return;
if (Reports.Count > 10)
Reports.RemoveFirst();
Reports.AddLast($"{DateTime.Now.ToLongTimeString()} Node {sender.ID}: {args.Report.ToString()!}");
}

private static async void Controller_NodeInfoUpdated(object? sender, ApplicationUpdateEventArgs e)
Expand All @@ -59,20 +138,17 @@ private static async void Controller_NodeInfoUpdated(object? sender, Application
if (node != null && !InterviewList.Contains(node.ID))
{
InterviewList.Add(node.ID);
CancellationTokenSource cts = new CancellationTokenSource(180000);
await Task.Factory.StartNew(async() => {
try
{
await node.Interview(cts.Token);
ReadyList.Add(node.ID);
await controller!.ExportNodeDBAsync("nodecache.db");
}
catch(Exception ex)
{
Log.Error(ex, "Uncaught Exception in Node Interview");
}
});
}
node.InterviewComplete += Node_InterviewComplete;
_ = Task.Run(() => node.Interview());
}
}

private static async void Node_InterviewComplete(object? sender, EventArgs e)
{
Node node = (Node)sender!;
ReadyList.Add(node.ID);
await controller!.ExportNodeDBAsync("nodecache.db");
AttachListeners(node);
}

private static async Task MainLoop()
Expand All @@ -87,7 +163,7 @@ private static async Task MainLoop()
private static void PrintMain()
{
Console.Clear();
Console.Write($"ZWaveDotNet v{Version} - Controller {controller!.ControllerID} {(controller!.IsConnected ? "Connected" : "Disconnected")}");
Console.Write($"ZWaveDotNet v{Version} - Controller #{controller!.ControllerID} {(controller!.IsConnected ? "Connected" : "Disconnected")}");
Console.Write($" - v{controller.APIVersion.Major} ({region})");
Console.Write($"{(controller!.SupportsLongRange ? " [LR]" : "")}");
Console.Write($"{(controller!.Primary ? " [Primary]" : "")}");
Expand All @@ -97,6 +173,24 @@ private static void PrintMain()
Console.WriteLine($"{controller.Nodes.Count} Nodes Found:");
foreach (Node n in controller.Nodes.Values)
Console.WriteLine(n.ToString());
Console.WriteLine();
if (currentMode == Mode.Display)
{
Console.WriteLine("Press I to enter Inclusion mode, E to enter Exclusion mode or S to Stop");
Console.WriteLine("Last 10 Node Reports:");
foreach (string report in Reports)
Console.WriteLine(report);
}
else if (currentMode == Mode.Inclusion)
{
Console.WriteLine("- Inclusion Mode Active (Default PIN 12345) -");
Console.WriteLine("Press the Pairing button on your device");
}
else
{
Console.WriteLine("- Exclusion Mode Active -");
Console.WriteLine("Press the Pairing button on your device");
}
}
}
}
20 changes: 9 additions & 11 deletions ZWaveDotNet/CommandClasses/CommandClassBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,13 +180,13 @@ public static CommandClassBase Create(CommandClass cc, Node node, byte endpoint,

protected async Task SendCommand(Enum command, CancellationToken token, params byte[] payload)
{
await SendCommand(command, token, false, payload);
await SendCommand(command, token, false, payload).ConfigureAwait(false);
}

protected async Task SendCommand(Enum command, CancellationToken token, bool supervised = false, params byte[] payload)
{
CommandMessage data = new CommandMessage(controller, node.ID, endpoint, commandClass, Convert.ToByte(command), supervised, payload);
await SendCommand(data, token);
await SendCommand(data, token).ConfigureAwait(false);
}

protected async Task SendCommand(CommandMessage data, CancellationToken token)
Expand All @@ -199,27 +199,27 @@ protected async Task SendCommand(CommandMessage data, CancellationToken token)
if (key == null)
throw new InvalidOperationException($"Command classes are secure but no keys exist for node {node.ID}");
if (key.Key == SecurityManager.RecordType.S0)
await node.GetCommandClass<Security0>()!.Encapsulate(data.Payload, token);
await node.GetCommandClass<Security0>()!.Encapsulate(data.Payload, token).ConfigureAwait(false);
else if (key.Key > SecurityManager.RecordType.S0)
await node.GetCommandClass<Security2>()!.Encapsulate(data.Payload, key.Key, token);
await node.GetCommandClass<Security2>()!.Encapsulate(data.Payload, key.Key, token).ConfigureAwait(false);
else
throw new InvalidOperationException("Security required but no keys are available");
}

DataMessage message = data.ToMessage();
for (int i = 0; i < 3; i++)
{
if (await AttemptTransmission(message, token, (i == 2)) == false)
if (await AttemptTransmission(message, token, i == 2).ConfigureAwait(false) == false)
{
Log.Error($"Transmission Failure: Retrying [Attempt {i+1}]...");
await Task.Delay(100 + (1000 * i), token);
Log.Error($"Controller Failed to Send Message: Retrying [Attempt {i+1}]...");
await Task.Delay(100 + (1000 * i), token).ConfigureAwait(false);
}
}
}

private async Task<bool> AttemptTransmission(DataMessage message, CancellationToken cancellationToken, bool ex = false)
{
DataCallback dc = await controller.Flow.SendAcknowledgedResponseCallback(message, cancellationToken);
DataCallback dc = await controller.Flow.SendAcknowledgedResponseCallback(message, cancellationToken).ConfigureAwait(false);
if (dc.Status != TransmissionStatus.CompleteOk && dc.Status != TransmissionStatus.CompleteNoAck && dc.Status != TransmissionStatus.CompleteVerified)
{
if (!ex)
Expand All @@ -241,7 +241,7 @@ public virtual Task Interview(CancellationToken cancellationToken)

protected async Task<ReportMessage> SendReceive(Enum command, Enum response, CancellationToken token, params byte[] payload)
{
return await SendReceive(command, response, token, false, payload);
return await SendReceive(command, response, token, false, payload).ConfigureAwait(false);
}

protected async Task<ReportMessage> SendReceive(Enum command, Enum response, CancellationToken token, bool supervised = false, params byte[] payload)
Expand All @@ -258,9 +258,7 @@ protected async Task<ReportMessage> SendReceive(Enum command, Enum response, Can
src
};
if (!callbacks.TryAdd(cmd, newCallbacks))
{
callbacks[cmd].Add(src);
}
}
await SendCommand(command, token, supervised, payload);
return await src.Task;
Expand Down
14 changes: 7 additions & 7 deletions ZWaveDotNet/CommandClasses/Security0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ public async Task<SupportedCommands> CommandsSupportedGet(CancellationToken canc
internal async Task SchemeGet(CancellationToken cancellationToken = default)
{
Log.Debug("Requesting Scheme");
await SendCommand(Security0Command.SchemeGet, cancellationToken, (byte)0x0);
await SendCommand(Security0Command.SchemeGet, cancellationToken, (byte)0x0).ConfigureAwait(false);
}

internal async Task KeySet(CancellationToken cancellationToken = default)
{
Log.Information($"Setting Network Key on {node.ID}");
CommandMessage data = new CommandMessage(controller, node.ID, endpoint, commandClass, (byte)Security0Command.NetworkKeySet, false, controller.NetworkKeyS0);
await TransmitTemp(data.Payload, cancellationToken);
await TransmitTemp(data.Payload, cancellationToken).ConfigureAwait(false);
}

protected async Task<ReportMessage> GetNonce(CancellationToken cancellationToken)
Expand All @@ -67,7 +67,7 @@ public async Task TransmitTemp(List<byte> payload, CancellationToken cancellatio
using (CancellationTokenSource timeout = new CancellationTokenSource(10000))
{
using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken);
report = await GetNonce(cts.Token);
report = await GetNonce(cts.Token).ConfigureAwait(false);
if (report.IsMulticastMethod())
return; //This should never happen
}
Expand All @@ -86,12 +86,12 @@ public async Task TransmitTemp(List<byte> payload, CancellationToken cancellatio
securePayload[8 + encrypted.Length] = receiversNonce[0];
Array.Copy(mac, 0, securePayload, 9 + encrypted.Length, 8);

await SendCommand(Security0Command.MessageEncap, cancellationToken, securePayload);
await SendCommand(Security0Command.MessageEncap, cancellationToken, securePayload).ConfigureAwait(false);
}

public async Task Encapsulate(List<byte> payload, CancellationToken cancellationToken)
{
ReportMessage report = await GetNonce(cancellationToken);
ReportMessage report = await GetNonce(cancellationToken).ConfigureAwait(false);
if (report.IsMulticastMethod())
return; //This should never happen

Expand Down Expand Up @@ -149,7 +149,7 @@ public async Task Encapsulate(List<byte> payload, CancellationToken cancellation
{
Log.Information("Providing Next Nonce");
using CancellationTokenSource cts = new CancellationTokenSource(3000);
await controller.Nodes[msg.SourceNodeID].GetCommandClass<Security0>()!.SendCommand(Security0Command.NonceReport, cts.Token, controller.SecurityManager.CreateS0Nonce(msg.SourceNodeID));
await controller.Nodes[msg.SourceNodeID].GetCommandClass<Security0>()!.SendCommand(Security0Command.NonceReport, cts.Token, controller.SecurityManager.CreateS0Nonce(msg.SourceNodeID)).ConfigureAwait(false);
}

Log.Information("Decrypted: " + msg.ToString());
Expand Down Expand Up @@ -179,7 +179,7 @@ protected override async Task<SupervisionStatus> Handle(ReportMessage message)
if (message.IsMulticastMethod())
return SupervisionStatus.Fail;

await SendCommand(Security0Command.NonceReport, CancellationToken.None, controller.SecurityManager.CreateS0Nonce(node.ID));
await SendCommand(Security0Command.NonceReport, CancellationToken.None, controller.SecurityManager.CreateS0Nonce(node.ID)).ConfigureAwait(false);
return SupervisionStatus.Success;
}
return SupervisionStatus.NoSupport;
Expand Down
Loading

0 comments on commit ede1e4a

Please sign in to comment.