OSDP.Net 5.2.1
A .NET library for implementing the Open Supervised Device Protocol (OSDP)
Loading...
Searching...
No Matches
PdMessageSecureChannel.cs
1using System;
2using System.Collections.ObjectModel;
3using System.Diagnostics;
4using System.Linq;
5using System.Threading;
6using System.Threading.Tasks;
7using Microsoft.Extensions.Logging;
13
15{
20 internal class PdMessageSecureChannelBase : MessageSecureChannel
21 {
32 public PdMessageSecureChannelBase(SecurityContext context = null, ILoggerFactory loggerFactory = null)
33 : base(context, loggerFactory) { }
34
36 public override void EncodePayload(byte[] payload, Span<byte> destination) =>
37 EncodePayload(payload, Context.CMac, destination);
38
40 public override byte[] DecodePayload(byte[] payload) => DecodePayload(payload, Context.RMac);
41
43 public override ReadOnlySpan<byte> GenerateMac(ReadOnlySpan<byte> message, bool isIncoming) =>
44 isIncoming ? GenerateCommandMac(message) : GenerateReplyMac(message);
45 }
46
47 internal class PdMessageSecureChannel(
48 IOsdpConnection connection,
49 SecurityContext context = null,
50 ILoggerFactory loggerFactory = null)
51 : PdMessageSecureChannelBase(context, loggerFactory)
52 {
53 private byte[] _expectedServerCryptogram;
54 private byte[] _securityKey;
55 private readonly byte[] _clientUID;
56
57 // When the last byte of the current command finished arriving, used to time the idle line
58 // delay owed before the reply may drive the channel.
59 private long _commandReceivedTimestamp;
60
61 public PdMessageSecureChannel(IOsdpConnection connection, byte[] securityKey, byte[] clientUID, ILoggerFactory loggerFactory = null)
62 : this(connection, context: null, loggerFactory)
63 {
64 _securityKey = securityKey;
65
66 if (clientUID == null)
67 {
68 throw new ArgumentNullException(nameof(clientUID));
69 }
70 if (clientUID.Length != 8)
71 {
72 throw new ArgumentException("Client UID must be exactly 8 bytes", nameof(clientUID));
73 }
74 _clientUID = clientUID;
75 }
76
77 public byte Address { get; set; }
78
79 public SecurityMode SecurityMode { get; set; } = SecurityMode.Unsecured;
80
81 public CommandType[] AllowUnsecured { get; set; } = [];
82
87 private TimeSpan ReplyIdleLineDelay => LineTiming.IdleLine(connection.BaudRate);
88
89 public async Task<IncomingMessage> ReadNextCommand(CancellationToken cancellationToken = default)
90 {
91 var commandBuffer = new Collection<byte>();
92
93 if (!await Bus.WaitForStartOfMessage(connection, commandBuffer, TimeSpan.FromSeconds(8), cancellationToken)
94 .ConfigureAwait(false))
95 {
96 return null;
97 }
98
99 if (!await Bus.WaitForMessageLength(connection, commandBuffer, cancellationToken).ConfigureAwait(false))
100 {
101 throw new TimeoutException("Timeout waiting for command message length");
102 }
103
104 if (!await Bus.WaitForRestOfMessage(connection, commandBuffer, Bus.ExtractMessageLength(commandBuffer),
105 cancellationToken).ConfigureAwait(false))
106 {
107 throw new TimeoutException("Timeout waiting for command of reply message");
108 }
109
110 // Stamped before any parsing so the idle line delay is measured from the wire, not from
111 // however long this PD's handlers take.
112 _commandReceivedTimestamp = Stopwatch.GetTimestamp();
113
114 var command = new IncomingMessage(commandBuffer.ToArray().AsSpan(), this);
115
116 if (command.Type != (byte)CommandType.Poll)
117 {
118 Logger?.LogInformation("Received Command: {CommandType}", Enum.GetName(typeof(CommandType), command.Type));
119 Logger?.LogDebug("Incoming: {Data}", BitConverter.ToString(commandBuffer.ToArray()));
120 }
121
122 var commandHandled = await HandleCommand(command);
123 return commandHandled ? await ReadNextCommand(cancellationToken) : command;
124 }
125
126 internal async Task SendReply(OutgoingReply reply, bool sendUnsecured = false)
127 {
128 if (reply.Command.Type == (byte)CommandType.KeySet && reply.Code == (byte)ReplyType.Ack)
129 {
130 HandleKeySetUpdate(reply);
131 }
132
133 var replyBuffer = reply.BuildMessage(sendUnsecured ? null : this);
134
135 // The line must be seen idle for two character times before this PD may drive it, or the
136 // ACU's transceiver may still be turning around when the reply's first bytes land.
137 await LineTiming.WaitAsync(ReplyIdleLineDelay, _commandReceivedTimestamp)
138 .ConfigureAwait(false);
139
140 if (reply.Command.Type != (byte)CommandType.Poll)
141 {
142 Logger?.LogInformation("Sending Reply: {Reply}", Enum.GetName(typeof(ReplyType), reply.PayloadData.Code));
143 Logger?.LogDebug("Outgoing: {Data}", BitConverter.ToString(replyBuffer));
144 }
145
146 await connection.WriteAsync(replyBuffer);
147 }
148
149 private async Task<bool> HandleCommand(IncomingMessage command)
150 {
151 if (command.Address != Address && command.Address != ControlPanel.ConfigurationAddress) return true;
152
153 DropStaleSecureSessionOnConnectionRestart(command);
154
155 var reply = (command.IsValidMac, (CommandType)command.Type) switch
156 {
157 (false, _) => HandleInvalidMac(),
158 (true, CommandType.SessionChallenge) => HandleSessionChallenge(command),
159 (true, CommandType.ServerCryptogram) => HandleSCrypt(command),
160 _ => ValidateCommandSecurity(command)
161 };
162
163 if (reply == null) return false;
164
165 // If we return NAK from here, it is generally because command didn't pass secure channel validation
166 // in this case we can only send the reply unsecured
167 await SendReply(new OutgoingReply(command, reply), reply.Code == (byte)ReplyType.Nak);
168
169 if (command.Type == (byte)CommandType.ServerCryptogram)
170 {
171 Context.IsSecurityEstablished = true;
172 }
173
174 return true;
175 }
183 internal void DropStaleSecureSessionOnConnectionRestart(IncomingMessage command)
184 {
185 if (command.Sequence == 0 && !command.IsSecureMessage && IsSecurityEstablished)
186 {
187 ResetSecureChannelSession();
188 }
189 }
190
191 private PayloadData HandleInvalidMac()
192 {
193 return new Nak(ErrorCode.CommunicationSecurityNotMet);
194 }
195
201 protected PayloadData HandleSessionChallenge(IncomingMessage command)
202 {
203 // Per Section D.1.3
204 bool useDefaultKey = command.SecureBlockData[0] == 0;
205 bool pdHasDefaultKey = _securityKey.SequenceEqual(SecurityContext.DefaultKey);
206
207 if (useDefaultKey && !pdHasDefaultKey)
208 {
209 // ACU requests SCBK-D, but PD has a non-default key configured
210 return new Nak(ErrorCode.DoesNotSupportSecurityBlock);
211 }
212
213 if (!useDefaultKey && pdHasDefaultKey)
214 {
215 // ACU requests a SCBK, but PD only has a default key configured
216 return new Nak(ErrorCode.DoesNotSupportSecurityBlock);
217 }
218
219 Context.Reset(_securityKey);
220
221 // generate a set of session keys: S-ENC, S-MAC1, S-MAC2 using command.Payload (which is RND.A)
222 using var crypto = Context.CreateCypher(true);
223 byte[] rndA = command.Payload;
224
225 // TODO: we should validate payload and SCB type
226
227 Context.Enc = SecurityContext.GenerateKey(crypto, [0x01, 0x82, rndA[0], rndA[1], rndA[2], rndA[3], rndA[4], rndA[5]
228 ]);
229 Context.SMac1 = SecurityContext.GenerateKey(crypto, [0x01, 0x01, rndA[0], rndA[1], rndA[2], rndA[3], rndA[4], rndA[5]
230 ]);
231 Context.SMac2 = SecurityContext.GenerateKey(crypto, [0x01, 0x02, rndA[0], rndA[1], rndA[2], rndA[3], rndA[4], rndA[5]
232 ]);
233
234 // Client UID is vendor code (3 bytes) + serial number (4 bytes) + padding (1 byte).
235 // See OSDP specification and GitHub issue #191.
236 byte[] cUID = _clientUID;
237 byte[] rndB = new byte[8];
238
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);
243
244 // reply with osdp_CCRYPT, returning PD's ID (cUID), its random number and the client cryptogram
245 return new ChallengeResponse(cUID, rndB, clientCryptogram, useDefaultKey);
246 }
247
253 protected PayloadData HandleSCrypt(IncomingMessage command)
254 {
255 var serverCryptogram = command.Payload;
256
257 if (command.SecurityBlockType != (byte)SecurityBlockType.SecureConnectionSequenceStep3)
258 {
259 Logger?.LogWarning("Received unexpected security block type: {SecurityBlockType}",
260 command.SecurityBlockType);
261 }
262 else if (!serverCryptogram.SequenceEqual(_expectedServerCryptogram))
263 {
264 Logger?.LogWarning("Received unexpected server cryptogram!");
265 }
266 else if (IsSecurityEstablished)
267 {
268 Logger?.LogWarning("Secure channel already established. Why did we get another SCrypt??");
269 }
270 else
271 {
272 var crypto = Context.CreateCypher(true, Context.SMac1);
273 Context.RMac = SecurityContext.GenerateKey(crypto, serverCryptogram);
274 crypto.Key = Context.SMac2;
275 Context.RMac = SecurityContext.GenerateKey(crypto, Context.RMac);
276
277 return new InitialRMac(Context.RMac, true);
278 }
279
280 return new Nak(ErrorCode.DoesNotSupportSecurityBlock);
281 }
282
283 private PayloadData ValidateCommandSecurity(IncomingMessage command)
284 {
285 if (IsSecurityEstablished)
286 {
287 return command.IsSecureMessage ? null : new Nak(ErrorCode.CommunicationSecurityNotMet);
288 }
289 else if (SecurityMode != SecurityMode.FullSecurity)
290 {
291 return null;
292 }
293 else
294 {
295 return AllowUnsecured.Contains((CommandType)command.Type) ?
296 null : new Nak(ErrorCode.CommunicationSecurityNotMet);
297 }
298 }
299
300 private void HandleKeySetUpdate(OutgoingReply reply)
301 {
302 var keySetPayload = EncryptionKeyConfiguration.ParseData(reply.Command.Payload);
303
304 _securityKey = keySetPayload.KeyData;
305 }
306 }
307
308 internal enum SecurityMode
309 {
310 Unsecured,
311 InstallMode,
312 FullSecurity
313 }
314}
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.
Definition ReplyType.cs:7
@ Nak
osdp_NAK - acknowledge; error response
Definition ReplyType.cs:16
@ InitialRMac
osdp_RMAC_I - client cryptogram packet and the initial R-MAC
Definition ReplyType.cs:91
CommandType
Identifies the type of OSDP protocol command (ACU -> PD) message.
Definition CommandType.cs:7
ErrorCode
Error code.
Definition Nak.cs:99