Skip to content

Commit

Permalink
Security improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
jdomnitz committed Sep 13, 2024
1 parent 11b97b7 commit 8c0b301
Show file tree
Hide file tree
Showing 8 changed files with 63 additions and 29 deletions.
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ An implementation of ZWave Plus using the 2024a public specification.
* See our [Examples Page](Examples.md)

#### Work in progress:
* Multicast is not yet exposed
* Security2 multicast is not implemented
* Multicast is not fully implemented (including secure multicast)
* Transport CC is receive only and not fully implemented (Very few devices use this)
* Node interviews are only partially implemented according to the spec

Testers, Tickets, Feedback and PRs are welcome.
19 changes: 8 additions & 11 deletions ZWaveDotNet/CommandClasses/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,31 +49,28 @@ public async Task<ConfigurationReport> Get(byte parameter, CancellationToken can

public async Task SetDefault(byte parameter, CancellationToken cancellationToken = default)
{
await Set(parameter, 0, 0, cancellationToken, true);
await Set(parameter, 0, cancellationToken, true);
}

public async Task Set(byte parameter, sbyte value, CancellationToken cancellationToken = default)
{
await Set(parameter, value, 0, cancellationToken);
await Set(parameter, value, cancellationToken);
}

public async Task Set(byte parameter, short value, CancellationToken cancellationToken = default)
{
await Set(parameter, value, 0, cancellationToken);
await Set(parameter, value, cancellationToken);
}

public async Task Set(byte parameter, int value, CancellationToken cancellationToken = default)
{
await Set(parameter, value, 0, cancellationToken);
await Set(parameter, value, cancellationToken);
}

private async Task Set(byte parameter, int value, byte size, CancellationToken cancellationToken = default, bool reset = false)
private async Task Set(byte parameter, int value, CancellationToken cancellationToken = default, bool resetToDefault = false)
{
if (size == 0)
{
ReportMessage response = await SendReceive(ConfigurationCommand.Get, ConfigurationCommand.Report, cancellationToken);
size = response.Payload.Span[1];
}
ReportMessage response = await SendReceive(ConfigurationCommand.Get, ConfigurationCommand.Report, cancellationToken);
byte size = response.Payload.Span[1];

var values = new byte[size];
switch (size)
Expand All @@ -90,7 +87,7 @@ private async Task Set(byte parameter, int value, byte size, CancellationToken c
default:
throw new NotSupportedException($"Size:{size} is not supported");
}
if (reset)
if (resetToDefault)
size |= 0x80;
await SendCommand(ConfigurationCommand.Set, cancellationToken, new[] { parameter, size }.Concat(values).ToArray());
}
Expand Down
20 changes: 17 additions & 3 deletions ZWaveDotNet/CommandClasses/Security2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,11 @@ public async Task Encapsulate(List<byte> payload, SecurityManager.RecordType? ty
decoded = Decrypt(msg, controller, networkKey, ad, ref i);
if (decoded != null)
break;
else if (controller.SecurityManager.HasKey(msg.SourceNodeID, SecurityManager.RecordType.ECDH_TEMP))
{
using (CancellationTokenSource cts = new CancellationTokenSource(3000))
await controller.Nodes[msg.SourceNodeID].GetCommandClass<Security2>()!.KexFail(KexFailType.KEX_FAIL_KEY_VERIFY).ConfigureAwait(false);
}
else if (i == 2)
{
try
Expand Down Expand Up @@ -364,13 +369,16 @@ protected override async Task<SupervisionStatus> Handle(ReportMessage message)
Log.Verbose("Kex Set Received: " + kexReport.ToString());
if (kexReport.Echo)
{
//kexReport is the granted keys
//TODO - Send KexFail if attempting to get more keys than we granted
if (controller.SecurityManager == null)
return SupervisionStatus.Fail;
KeyExchangeReport? requestedKeys = controller.SecurityManager.GetRequestedKeys(node.ID);
if (requestedKeys != null)
{
if (requestedKeys.Keys != kexReport.Keys)
{
await KexFail(KexFailType.KEX_FAIL_AUTH);
return SupervisionStatus.Fail;
}
requestedKeys.Echo = true;
Log.Verbose("Responding: " + requestedKeys.ToString());
CommandMessage reportKex = new CommandMessage(controller, node.ID, endpoint, commandClass, (byte)Security2Command.KEXReport, false, requestedKeys.ToBytes());
Expand All @@ -393,7 +401,13 @@ protected override async Task<SupervisionStatus> Handle(ReportMessage message)
Log.Verbose("Network Key Get Received");
byte[] resp = new byte[17];
SecurityKey key = (SecurityKey)message.Payload.Span[0];
//TODO - Verify this was granted
KeyExchangeReport? grantedKeys = controller.SecurityManager.GetRequestedKeys(node.ID);
if (grantedKeys == null || (grantedKeys.Keys & key) != key)
{
await KexFail(KexFailType.KEX_FAIL_KEY_GET);
Log.Error("Network Key Get Received for an ungranted key");
return SupervisionStatus.Fail;
}
resp[0] = (byte)key;
switch (key)
{
Expand Down
8 changes: 4 additions & 4 deletions ZWaveDotNet/CommandClasses/TransportService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,15 @@ public static void Transmit (List<byte> payload)
sessionId = (byte)((msg.Payload.Span[1] & 0xF0) >> 4);
if ((msg.Payload.Span[1] & 0x8) == 0x8)
{
//We skip extensions for now
//No extensions are defined yet
Log.Information("Transport Service skipped an extension");
msg.Payload = msg.Payload.Slice(msg.Payload.Span[2] + 3);
}
else
msg.Payload = msg.Payload.Slice(2);
chk = crc.ComputeChecksum(msg.Payload.Slice(0, msg.Payload.Length - 2));
if (chk[0] == msg.Payload.Span[msg.Payload.Length - 2] && chk[1] == msg.Payload.Span[msg.Payload.Length - 1])
Log.Verbose("Transport Checksum is OK");
Log.Debug("Transport Checksum is OK");
buff = new byte[datagramLen];
msg.Payload.Slice(0, msg.Payload.Length - 2).CopyTo(buff);
key = GetKey(msg.SourceNodeID, sessionId);
Expand All @@ -101,15 +101,15 @@ public static void Transmit (List<byte> payload)
ushort datagramOffset = (ushort)(((msg.Payload.Span[1] & 0x7) << 8) | msg.Payload.Span[2]);
if ((msg.Payload.Span[1] & 0x8) == 0x8)
{
//We skip extensions for now
//No extensions are defined yet
Log.Information("Transport Service skipped an extension");
msg.Payload = msg.Payload.Slice(msg.Payload.Span[3] + 4);
}
else
msg.Payload = msg.Payload.Slice(3);
chk = crc.ComputeChecksum(msg.Payload.Slice(0, msg.Payload.Length - 2));
if (chk[0] == msg.Payload.Span[msg.Payload.Length - 2] && chk[1] == msg.Payload.Span[msg.Payload.Length - 1])
Log.Verbose("Transport Checksum is OK");
Log.Debug("Transport Checksum is OK");
key = GetKey(msg.SourceNodeID, sessionId);
if (!buffers.TryGetValue(key, out buff))
{
Expand Down
2 changes: 1 addition & 1 deletion ZWaveDotNet/Entities/Controller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ public async Task<Memory<byte>> GetRandom(byte length, CancellationToken cancell
if (random == null || random.Data.Span[0] == 0x0) //TODO - Status Enums
{
Memory<byte> planB = new byte[length];
new Random().NextBytes(planB.Span);
RandomNumberGenerator.Fill(planB.Span);
return planB;
}
return random!.Data.Slice(2);
Expand Down
8 changes: 4 additions & 4 deletions ZWaveDotNet/Security/AES.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ public static class AES
public struct KeyTuple
{
public byte[] KeyCCM;
public byte[] PString;
public byte[] MPAN;
public byte[] PersonalizationString;
public byte[] keyMPAN;
public KeyTuple(byte[] keyCCM, byte[] pString, byte[] mPAN)
{
this.KeyCCM = keyCCM;
this.PString = pString;
this.MPAN = mPAN;
this.PersonalizationString = pString;
this.keyMPAN = mPAN;
}
}

Expand Down
22 changes: 21 additions & 1 deletion ZWaveDotNet/Security/SecurityManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public void GrantKey(ushort nodeId, RecordType type, byte[]? key = null, bool te
if (key == null)
throw new ArgumentNullException(nameof(key));
AES.KeyTuple keyTuple = AES.CKDFExpand(key, temp);
StoreKey(nodeId, type, keyTuple.KeyCCM, keyTuple.PString, keyTuple.MPAN);
StoreKey(nodeId, type, keyTuple.KeyCCM, keyTuple.PersonalizationString, keyTuple.keyMPAN);
}

private void StoreKey(ushort nodeId, RecordType type, byte[]? keyCCM, byte[]? pString, byte[]? mPAN)
Expand Down Expand Up @@ -143,6 +143,11 @@ public RecordType[] GetKeys(ushort nodeId)
return null;
}

public bool HasKey(ushort nodeId, RecordType key)
{
return GetKey(nodeId, key) != null;
}

public void RevokeKey(ushort nodeId, RecordType type)
{
if (keys.TryGetValue(nodeId, out List<NetworkKey>? keyLst))
Expand Down Expand Up @@ -217,6 +222,21 @@ public bool IsSequenceNew(ushort nodeId, byte sequence)
return null;
}

public Memory<byte>? CurrentMpanNonce(byte groupID, byte[] keyMPAN)
{
if (mpanRecords.TryGetValue(groupID, out MpanRecord? record))
{
Memory<byte> result = new byte[16];
using (Aes aes = Aes.Create())
{
aes.Key = keyMPAN;
aes.EncryptEcb(record.Bytes.Span, result.Span, PaddingMode.None);
}
return result;
}
return null;
}

public Memory<byte>? NextMpanNonce(byte groupID, byte[] keyMPAN)
{
if (mpanRecords.TryGetValue(groupID, out MpanRecord? record))
Expand Down
9 changes: 7 additions & 2 deletions ZWaveDotNet/Util/MemoryUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,13 @@ public static void Increment(Span<byte> mem)

public static string Print(Memory<byte> mem)
{
StringBuilder ret = new StringBuilder(mem.Length * 3);
foreach (byte b in mem.Span)
return Print(mem.Span);
}

public static string Print(ReadOnlySpan<byte> span)
{
StringBuilder ret = new StringBuilder(span.Length * 3);
foreach (byte b in span)
{
if (ret.Length > 0)
ret.Append(' ');
Expand Down

0 comments on commit 8c0b301

Please sign in to comment.