OSDP.Net 5.2.1
A .NET library for implementing the Open Supervised Device Protocol (OSDP)
Loading...
Searching...
No Matches
DeviceProxy.cs
1using System;
2using System.Collections.Concurrent;
3using System.Linq;
4using System.Runtime.CompilerServices;
8
9[assembly: InternalsVisibleTo("OSDP.Net.Tests")]
10namespace OSDP.Net;
11
12internal class DeviceProxy : IComparable<DeviceProxy>
13{
14 private const int RetryAmount = 2;
15 private const int BusyRetryAmount = 20;
16
17 private readonly ConcurrentQueue<CommandData> _commands = new();
18 private readonly bool _useSecureChannel;
19
20 private int _counter = RetryAmount;
21 private bool _counterIsBusyRetry;
22 private DateTime _lastValidReply = DateTime.MinValue;
23 private CommandData _retryCommand;
24
32 public DeviceProxy(byte address, bool useCrc, bool useSecureChannel, byte[] secureChannelKey = null)
33 {
34 _useSecureChannel = useSecureChannel;
35
36 Address = address;
37 MessageControl = new Control(0, useCrc, useSecureChannel);
38
39 if (UseSecureChannel)
40 {
41 SecureChannelKey = secureChannelKey ?? SecurityContext.DefaultKey;
42
43 IsDefaultKey = SecurityContext.DefaultKey.SequenceEqual(SecureChannelKey);
44
45 MessageSecureChannel = new ACUMessageSecureChannel(new SecurityContext(SecureChannelKey));
46 }
47 else
48 {
49 MessageSecureChannel = new ACUMessageSecureChannel(new SecurityContext());
50 }
51 }
52
53 internal byte[] SecureChannelKey { get; }
54
55 private bool IsDefaultKey { get; }
56
57 public byte Address { get; }
58
59 internal Control MessageControl { get; }
60
61 public bool UseSecureChannel => !IsSendingMultiMessageNoSecureChannel && _useSecureChannel;
62
63 public bool IsSecurityEstablished => !IsSendingMultiMessageNoSecureChannel && MessageControl.HasSecurityControlBlock && MessageSecureChannel.IsSecurityEstablished;
64
65 public bool IsConnected => _lastValidReply + TimeSpan.FromSeconds(8) >= DateTime.UtcNow &&
66 (IsSendingMultiMessageNoSecureChannel || !MessageControl.HasSecurityControlBlock || IsSecurityEstablished);
67
68 public IMessageSecureChannel MessageSecureChannel { get; }
69
70 internal bool IsSendingMultipartMessage { get; set; }
71
72 internal DateTime RequestDelay { get; set; }
73
74 internal bool IsSendingMultiMessageNoSecureChannel { get; set; }
75
76 internal bool IsReceivingMultipartMessage { get; set; }
77
81 internal bool HasQueuedCommand => _commands.Any();
82
84 public int CompareTo(DeviceProxy other)
85 {
86 if (ReferenceEquals(this, other)) return 0;
87 if (ReferenceEquals(null, other)) return 1;
88 return Address.CompareTo(other.Address);
89 }
90
96 internal virtual OutgoingMessage GetNextCommandData(bool isPolling)
97 {
98 if (_retryCommand != null)
99 {
100 var saveCommand = _retryCommand;
101 _retryCommand = null;
102 return new OutgoingMessage(Address, MessageControl, saveCommand);
103 }
104
105 if (isPolling)
106 {
107 // Don't send clear text polling if using secure channel
108 if (MessageControl.Sequence == 0 && !UseSecureChannel)
109 {
110 return new OutgoingMessage(Address, MessageControl, new NoPayloadCommandData(CommandType.Poll));
111 }
112
113 if (UseSecureChannel && !MessageSecureChannel.IsInitialized)
114 {
115 return new OutgoingMessage(Address, MessageControl,
116 new SecurityInitialization(MessageSecureChannel.ServerRandomNumber, IsDefaultKey));
117 }
118
119 if (UseSecureChannel && !MessageSecureChannel.IsSecurityEstablished)
120 {
121 return new OutgoingMessage(Address, MessageControl,
122 new ServerCryptogramData(MessageSecureChannel.ServerCryptogram, IsDefaultKey));
123 }
124 }
125
126 if (!_commands.TryDequeue(out var command) && isPolling)
127 {
128 return new OutgoingMessage(Address, MessageControl, new NoPayloadCommandData(CommandType.Poll));
129 }
130
131 return command != null
132 ? new OutgoingMessage(Address, MessageControl, command)
133 : new OutgoingMessage(Address, MessageControl, new NoPayloadCommandData(CommandType.Poll));
134 }
135
136 internal void SendCommand(CommandData command)
137 {
138 _commands.Enqueue(command);
139 }
140
146 internal void RetryCommand(CommandData command, bool isBusyRetry = false)
147 {
148 if (_counterIsBusyRetry != isBusyRetry)
149 {
150 _counter = isBusyRetry ? BusyRetryAmount : RetryAmount;
151 _counterIsBusyRetry = isBusyRetry;
152 }
153
154 if (_counter-- > 0)
155 {
156 _retryCommand = command;
157 }
158 else
159 {
160 _retryCommand = null;
161 _counter = isBusyRetry ? BusyRetryAmount : RetryAmount;
162 }
163 }
164
165 internal void ValidReplyHasBeenReceived(byte sequence)
166 {
167 MessageControl.IncrementSequence(sequence);
168
169 // It's valid once sequences are above zero
170 if (sequence > 0) _lastValidReply = DateTime.UtcNow;
171
172 // Reset retry counter
173 _counter = RetryAmount;
174 _counterIsBusyRetry = false;
175 }
176
177 internal void InitializeSecureChannel(byte[] payload, byte[] secureBlockData)
178 {
179 // Validate payload: must be 32 bytes (cUID[8] + clientRnd[8] + clientCryptogram[16])
180 if (payload == null || payload.Length < 32)
181 {
182 throw new InvalidPayloadException(
183 $"osdp_CCRYPT payload must be 32 bytes, received {payload?.Length ?? 0}");
184 }
185
186 // Validate SCB key type: secureBlockData[0] indicates SCBK-D (0x00) or SCBK (0x01)
187 // This must match the key type the ACU sent in osdp_CHLNG
188 if (secureBlockData == null || secureBlockData.Length == 0)
189 {
190 throw new InvalidPayloadException(
191 "osdp_CCRYPT response missing security control block data for key type validation");
192 }
193
194 bool pdClaimsDefaultKey = secureBlockData[0] == 0x00;
195 if (IsDefaultKey != pdClaimsDefaultKey)
196 {
197 throw new SecureChannelKeyTypeMismatchException(IsDefaultKey, pdClaimsDefaultKey);
198 }
199
200 MessageSecureChannel.InitializeACU(payload.Skip(8).Take(8).ToArray(),
201 payload.Skip(16).Take(16).ToArray());
202 }
203
204 internal void ResetSecureChannelSession()
205 {
206 MessageSecureChannel.ResetSecureChannelSession();
207 }
208
209 internal ReadOnlySpan<byte> GenerateMac(ReadOnlySpan<byte> message, bool isIncoming)
210 {
211 return MessageSecureChannel.GenerateMac(message, isIncoming);
212 }
213}
214
215internal interface IDeviceProxyFactory
216{
217 DeviceProxy Create(byte address, bool useCrc, bool useSecureChannel, byte[] secureChannelKey = null);
218}
219
220internal class DeviceProxyFactory : IDeviceProxyFactory
221{
222 public DeviceProxy Create(
223 byte address, bool useCrc, bool useSecureChannel, byte[] secureChannelKey = null)
224 => new DeviceProxy(address, useCrc, useSecureChannel, secureChannelKey);
225}
CommandType
Identifies the type of OSDP protocol command (ACU -> PD) message.
Definition CommandType.cs:7
@ Poll
osdp_POLL - poll request