2using System.Collections.ObjectModel;
3using System.Diagnostics;
6using System.Threading.Tasks;
7using Microsoft.Extensions.Logging;
32 public PdMessageSecureChannelBase(SecurityContext context =
null, ILoggerFactory loggerFactory =
null)
33 : base(context, loggerFactory) { }
36 public override void EncodePayload(
byte[] payload, Span<byte> destination) =>
37 EncodePayload(payload,
Context.CMac, destination);
40 public override byte[] DecodePayload(
byte[] payload) => DecodePayload(payload,
Context.RMac);
43 public override ReadOnlySpan<byte> GenerateMac(ReadOnlySpan<byte> message,
bool isIncoming) =>
47 internal class PdMessageSecureChannel(
48 IOsdpConnection connection,
50 ILoggerFactory loggerFactory =
null)
51 : PdMessageSecureChannelBase(context, loggerFactory)
53 private byte[] _expectedServerCryptogram;
54 private byte[] _securityKey;
55 private readonly
byte[] _clientUID;
59 private long _commandReceivedTimestamp;
61 public PdMessageSecureChannel(IOsdpConnection connection,
byte[] securityKey,
byte[] clientUID, ILoggerFactory loggerFactory =
null)
62 : this(connection, context: null, loggerFactory)
64 _securityKey = securityKey;
66 if (clientUID ==
null)
68 throw new ArgumentNullException(nameof(clientUID));
70 if (clientUID.Length != 8)
72 throw new ArgumentException(
"Client UID must be exactly 8 bytes", nameof(clientUID));
74 _clientUID = clientUID;
77 public byte Address {
get;
set; }
79 public SecurityMode SecurityMode {
get;
set; } = SecurityMode.Unsecured;
81 public CommandType[] AllowUnsecured {
get;
set; } = [];
87 private TimeSpan ReplyIdleLineDelay => LineTiming.IdleLine(connection.
BaudRate);
89 public async Task<IncomingMessage> ReadNextCommand(CancellationToken cancellationToken =
default)
91 var commandBuffer =
new Collection<byte>();
93 if (!await Bus.WaitForStartOfMessage(connection, commandBuffer, TimeSpan.FromSeconds(8), cancellationToken)
94 .ConfigureAwait(
false))
99 if (!await Bus.WaitForMessageLength(connection, commandBuffer, cancellationToken).ConfigureAwait(
false))
101 throw new TimeoutException(
"Timeout waiting for command message length");
104 if (!await Bus.WaitForRestOfMessage(connection, commandBuffer, Bus.ExtractMessageLength(commandBuffer),
105 cancellationToken).ConfigureAwait(
false))
107 throw new TimeoutException(
"Timeout waiting for command of reply message");
112 _commandReceivedTimestamp = Stopwatch.GetTimestamp();
114 var command =
new IncomingMessage(commandBuffer.ToArray().AsSpan(),
this);
118 Logger?.LogInformation(
"Received Command: {CommandType}", Enum.GetName(typeof(
CommandType), command.Type));
119 Logger?.LogDebug(
"Incoming: {Data}", BitConverter.ToString(commandBuffer.ToArray()));
122 var commandHandled = await HandleCommand(command);
123 return commandHandled ? await ReadNextCommand(cancellationToken) : command;
126 internal async Task SendReply(OutgoingReply reply,
bool sendUnsecured =
false)
130 HandleKeySetUpdate(reply);
133 var replyBuffer = reply.BuildMessage(sendUnsecured ?
null :
this);
137 await LineTiming.WaitAsync(ReplyIdleLineDelay, _commandReceivedTimestamp)
138 .ConfigureAwait(
false);
142 Logger?.LogInformation(
"Sending Reply: {Reply}", Enum.GetName(typeof(
ReplyType), reply.PayloadData.Code));
143 Logger?.LogDebug(
"Outgoing: {Data}", BitConverter.ToString(replyBuffer));
149 private async Task<bool> HandleCommand(IncomingMessage command)
151 if (command.Address != Address && command.Address != ControlPanel.ConfigurationAddress)
return true;
153 DropStaleSecureSessionOnConnectionRestart(command);
155 var reply = (command.IsValidMac, (
CommandType)command.Type)
switch
157 (
false, _) => HandleInvalidMac(),
158 (
true,
CommandType.SessionChallenge) => HandleSessionChallenge(command),
159 (
true,
CommandType.ServerCryptogram) => HandleSCrypt(command),
160 _ => ValidateCommandSecurity(command)
163 if (reply ==
null)
return false;
167 await SendReply(
new OutgoingReply(command, reply), reply.Code == (
byte)
ReplyType.Nak);
169 if (command.Type == (
byte)
CommandType.ServerCryptogram)
171 Context.IsSecurityEstablished =
true;
183 internal void DropStaleSecureSessionOnConnectionRestart(IncomingMessage command)
185 if (command.Sequence == 0 && !command.IsSecureMessage && IsSecurityEstablished)
187 ResetSecureChannelSession();
191 private PayloadData HandleInvalidMac()
201 protected PayloadData HandleSessionChallenge(IncomingMessage command)
204 bool useDefaultKey = command.SecureBlockData[0] == 0;
205 bool pdHasDefaultKey = _securityKey.SequenceEqual(
SecurityContext.DefaultKey);
207 if (useDefaultKey && !pdHasDefaultKey)
213 if (!useDefaultKey && pdHasDefaultKey)
219 Context.Reset(_securityKey);
222 using var crypto = Context.CreateCypher(
true);
223 byte[] rndA = command.Payload;
227 Context.Enc =
SecurityContext.GenerateKey(crypto, [0x01, 0x82, rndA[0], rndA[1], rndA[2], rndA[3], rndA[4], rndA[5]
229 Context.SMac1 =
SecurityContext.GenerateKey(crypto, [0x01, 0x01, rndA[0], rndA[1], rndA[2], rndA[3], rndA[4], rndA[5]
231 Context.SMac2 =
SecurityContext.GenerateKey(crypto, [0x01, 0x02, rndA[0], rndA[1], rndA[2], rndA[3], rndA[4], rndA[5]
236 byte[] cUID = _clientUID;
237 byte[] rndB =
new byte[8];
239 new Random().NextBytes(rndB);
240 crypto.Key = Context.Enc;
241 var clientCryptogram =
SecurityContext.GenerateKey(crypto, rndA, rndB);
242 _expectedServerCryptogram =
SecurityContext.GenerateKey(crypto, rndB, rndA);
245 return new ChallengeResponse(cUID, rndB, clientCryptogram, useDefaultKey);
253 protected PayloadData HandleSCrypt(IncomingMessage command)
255 var serverCryptogram = command.Payload;
257 if (command.SecurityBlockType != (
byte)
SecurityBlockType.SecureConnectionSequenceStep3)
259 Logger?.LogWarning(
"Received unexpected security block type: {SecurityBlockType}",
260 command.SecurityBlockType);
262 else if (!serverCryptogram.SequenceEqual(_expectedServerCryptogram))
264 Logger?.LogWarning(
"Received unexpected server cryptogram!");
266 else if (IsSecurityEstablished)
268 Logger?.LogWarning(
"Secure channel already established. Why did we get another SCrypt??");
272 var crypto = Context.CreateCypher(
true, Context.SMac1);
274 crypto.Key = Context.SMac2;
283 private PayloadData ValidateCommandSecurity(IncomingMessage command)
285 if (IsSecurityEstablished)
287 return command.IsSecureMessage ? null :
new Nak(
ErrorCode.CommunicationSecurityNotMet);
289 else if (SecurityMode != SecurityMode.FullSecurity)
295 return AllowUnsecured.Contains((
CommandType)command.Type) ?
300 private void HandleKeySetUpdate(OutgoingReply reply)
302 var keySetPayload = EncryptionKeyConfiguration.ParseData(reply.Command.Payload);
304 _securityKey = keySetPayload.KeyData;
308 internal enum SecurityMode
V2 implementation of a SecureChannel class.
byte[] GenerateCommandMac(ReadOnlySpan< byte > message)
Generates a MAC for a command message.
byte[] GenerateReplyMac(ReadOnlySpan< byte > message)
Generates a MAC for a reply message.
SecurityContext Context
Security state used by the channel.
Security context used within MessageSecureChannel.
Task WriteAsync(byte[] buffer)
Write to connection.
int BaudRate
Speed of the connection.
SecurityBlockType
Security Block Type values as defined by OSDP protocol.
ReplyType
Identifies the type of OSDP protocol reply (PD -> ACU) message.
@ Nak
osdp_NAK - acknowledge; error response
@ InitialRMac
osdp_RMAC_I - client cryptogram packet and the initial R-MAC
CommandType
Identifies the type of OSDP protocol command (ACU -> PD) message.