OSDP.Net 5.2.1
A .NET library for implementing the Open Supervised Device Protocol (OSDP)
Loading...
Searching...
No Matches
MessageSpy.cs
1#nullable enable
2
3using System;
4using System.Collections.Generic;
5using System.Dynamic;
6using System.Linq;
7using System.Text.Json;
10using OSDP.Net.Model;
13
14namespace OSDP.Net.Tracing;
15
20public class MessageSpy
21{
22 private const byte MarkByte = 0xFF;
23
24 private readonly SecurityContext _context;
25 private readonly MessageSecureChannel _commandSpyChannel;
26 private readonly MessageSecureChannel _replySpyChannel;
27
32 public MessageSpy(byte[]? securityKey = null)
33 {
34 _context = new SecurityContext(securityKey);
35 _commandSpyChannel = new PdMessageSecureChannelBase(_context);
36 _replySpyChannel = new ACUMessageSecureChannel(_context);
37 }
38
44 public byte PeekAddressByte(ReadOnlySpan<byte> data)
45 {
46 return SkipMarkBytes(data)[1];
47 }
48
55 private static ReadOnlySpan<byte> SkipMarkBytes(ReadOnlySpan<byte> data)
56 {
57 int index = 0;
58 while (index < data.Length && data[index] == MarkByte)
59 {
60 index++;
61 }
62
63 return data.Slice(index);
64 }
65
71 public IncomingMessage ParseCommand(byte[] data)
72 {
73 var command = new IncomingMessage(SkipMarkBytes(data), _commandSpyChannel);
74
75 return (CommandType)command.Type switch
76 {
77 CommandType.SessionChallenge => HandleSessionChallenge(command),
78 CommandType.ServerCryptogram => HandleSCrypt(command),
79 CommandType.KeySet => HandleKeySet(command),
80 _ => command
81 };
82 }
83
84 private IncomingMessage HandleKeySet(IncomingMessage command)
85 {
86 // osdp_KEYSET installs a new Secure Channel Base Key. The command is delivered over the
87 // current secure channel, so once its payload has been decrypted we capture the new key and
88 // apply it to the shared security context. The ACU then tears down and re-establishes the
89 // secure channel, and that handshake must derive its session keys (S-ENC/S-MAC) from this
90 // new key rather than the one originally supplied to the parser. Without this, every
91 // encrypted payload after the re-key decodes to garbage (e.g. "missing a padding byte").
92 if (command.IsPayloadDecrypted)
93 {
94 var keyConfiguration = EncryptionKeyConfiguration.ParseData(command.Payload);
95 _context.UpdateSecurityKey(keyConfiguration.KeyData);
96 }
97
98 return command;
99 }
100
106 public IncomingMessage ParseReply(byte[] data)
107 {
108 var reply = new IncomingMessage(SkipMarkBytes(data), _replySpyChannel);
109
110 return (ReplyType)reply.Type switch
111 {
112 ReplyType.InitialRMac => HandleInitialRMac(reply),
113 _ => reply
114 };
115 }
116
117 private IncomingMessage HandleSessionChallenge(IncomingMessage command)
118 {
119 // The osdp_CHLNG security block data byte is the authoritative indicator of which key the
120 // session uses: 0x00 = well-known default key (SCBK-D), anything else = per-installation key
121 // (SCBK). Record it so every subsequent secure message in this session reports the actual
122 // wire key type rather than the key that happened to be supplied to the parser.
123 if (command.SecureBlockData is { Length: > 0 })
124 {
125 bool isUsingDefaultKey = command.SecureBlockData[0] == 0x00;
126 _context.IsUsingDefaultKey = isUsingDefaultKey;
127 // The command message was constructed before the context was updated above, so re-stamp
128 // it too, keeping the osdp_CHLNG packet consistent with the rest of the session.
129 command.IsUsingDefaultKey = isUsingDefaultKey;
130 }
131
132 byte[] rndA = command.Payload;
133 var crypto = _context.CreateCypher(true);
134 _context.Enc = SecurityContext.GenerateKey(crypto,
135 [0x01, 0x82, rndA[0], rndA[1], rndA[2], rndA[3], rndA[4], rndA[5]]);
136 _context.SMac1 = SecurityContext.GenerateKey(crypto,
137 [0x01, 0x01, rndA[0], rndA[1], rndA[2], rndA[3], rndA[4], rndA[5]]);
138 _context.SMac2 = SecurityContext.GenerateKey(crypto,
139 [0x01, 0x02, rndA[0], rndA[1], rndA[2], rndA[3], rndA[4], rndA[5]]);
140 return command;
141 }
142
143 private IncomingMessage HandleSCrypt(IncomingMessage command)
144 {
145 var serverCryptogram = command.Payload;
146 using var crypto = _context.CreateCypher(true, _context.SMac1);
147 var intermediate = SecurityContext.GenerateKey(crypto, serverCryptogram);
148 crypto.Key = _context.SMac2;
149 _context.RMac = SecurityContext.GenerateKey(crypto, intermediate);
150 return command;
151 }
152
153 private IncomingMessage HandleInitialRMac(IncomingMessage reply)
154 {
155 _context.IsSecurityEstablished = true;
156 return reply;
157 }
158
164 public Packet ParsePacket(byte[] data)
165 {
166 const byte replyAddress = 0x80;
167 var message = PeekAddressByte(data) < replyAddress
168 ? ParseCommand(data)
169 : ParseReply(data);
170 return new Packet(message);
171 }
172
179 public bool TryParsePacket(byte[] data, out Packet? packet)
180 {
181 try
182 {
183 packet = ParsePacket(data);
184 return true;
185 }
186 catch
187 {
188 packet = null;
189 return false;
190 }
191 }
192
198 public IEnumerable<OSDPCaptureEntry> ParseCaptureFile(string json)
199 {
200 var lines = json.Split('\n');
201 foreach (var line in lines)
202 {
203 if (string.IsNullOrWhiteSpace(line)) continue;
204
205 dynamic? entry = JsonSerializer.Deserialize<ExpandoObject>(line);
206 if (entry == null) continue;
207
208 DateTime dateTime = new DateTime(1970, 1, 1).AddSeconds(double.Parse(entry.timeSec.ToString()))
209 .AddTicks(long.Parse(entry.timeNano.ToString()) / 100L);
210 Enum.TryParse(entry.io.ToString(), true, out TraceDirection io);
211 string data = entry.data.ToString();
212
213 var rawData = BinaryUtils.HexToBytes(data).ToArray();
214 var packet = ParsePacket(rawData);
215
216 yield return new OSDPCaptureEntry(
217 dateTime,
218 io,
219 packet,
220 entry.osdpTraceVersion.ToString(),
221 entry.osdpSource.ToString());
222 }
223 }
224}
Represents a message that was received from the wire.
bool IsPayloadDecrypted
Indicates whether the payload was successfully decrypted.
byte[] SecureBlockData
Raw security block bytes.
byte[] Payload
Returns the raw message payload.
Message channel which represents the Access Control Unit (ACU) side of the OSDP communications (i....
V2 implementation of a SecureChannel class.
Security context used within MessageSecureChannel.
Command data to set the encryption key configuration on a PD.
static EncryptionKeyConfiguration ParseData(ReadOnlySpan< byte > data)
Parses the message payload bytes.
Information details of a message packet.
Definition Packet.cs:14
IEnumerable< OSDPCaptureEntry > ParseCaptureFile(string json)
Parses an OSDP capture file in JSON format.
bool TryParsePacket(byte[] data, out Packet? packet)
Attempts to parse raw OSDP data into a Packet without throwing exceptions.
byte PeekAddressByte(ReadOnlySpan< byte > data)
Retrieves the address byte from raw OSDP data without parsing the entire message.
Definition MessageSpy.cs:44
Packet ParsePacket(byte[] data)
Parses raw OSDP data into a Packet, automatically detecting command vs.
IncomingMessage ParseCommand(byte[] data)
Parses raw OSDP command data into an IncomingMessage, handling the secure channel establishment.
Definition MessageSpy.cs:71
IncomingMessage ParseReply(byte[] data)
Parses raw OSDP reply data into an IncomingMessage, handling the secure channel establishment.
MessageSpy(byte[]? securityKey=null)
Initializes a new instance of the MessageSpy class with an optional security key.
Definition MessageSpy.cs:32
One line entry of a file that captures OSDP messages.
Helper routines for manipulating binary representations of data that are used within the OSDP....
static IEnumerable< byte > HexToBytes(IEnumerable< char > seq)
Convert a sequence of hex digit characters into their byte representation.
ReplyType
Identifies the type of OSDP protocol reply (PD -> ACU) message.
Definition ReplyType.cs:7
@ 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
@ ServerCryptogram
osdp_SCRYPT - server's random number and server cryptogram
@ KeySet
osdp_KEYSET - Encryption key set
@ SessionChallenge
osdp_CHLNG - challenge and secure session initialization request
TraceDirection
Direction of trace.
Definition TraceEntry.cs:9