Skip to content

Commit 93c2baa

Browse files
committed
Optimize a few hot spots
1 parent 431f36c commit 93c2baa

3 files changed

Lines changed: 184 additions & 41 deletions

File tree

SharpSnmpLib/DataFactory.cs

Lines changed: 97 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,12 @@ public static IAsnSerializable CreateSnmpData(byte[] buffer, int index, int coun
4646
throw new SnmpException("empty data buffer");
4747
}
4848

49-
var slice = new byte[count];
50-
Buffer.BlockCopy(buffer, index, slice, 0, count);
51-
return Parse(slice);
49+
if (index == 0 && count == buffer.Length)
50+
{
51+
return Parse(buffer);
52+
}
53+
54+
return Parse(new ReadOnlyMemory<byte>(buffer, index, count));
5255
}
5356

5457
/// <summary>
@@ -90,6 +93,11 @@ public static IAsnSerializable CreateSnmpData(int type, Stream stream)
9093
}
9194

9295
private static IAsnSerializable Parse(byte[] payload)
96+
{
97+
return Parse(new ReadOnlyMemory<byte>(payload));
98+
}
99+
100+
private static IAsnSerializable Parse(ReadOnlyMemory<byte> payload)
93101
{
94102
try
95103
{
@@ -212,11 +220,20 @@ private static IAsnSerializable Parse(byte[] payload)
212220

213221
if (tag.HasSameClassAndValue(Asn1Tag.Sequence))
214222
{
215-
try
223+
var sequenceKind = ClassifySequence(trimmedPayload);
224+
if (sequenceKind == SequenceKind.Scope)
216225
{
217-
return Scope.ReadFrom(new AsnReader(trimmedPayload, AsnEncodingRules.BER));
226+
try
227+
{
228+
return Scope.ReadFrom(new AsnReader(trimmedPayload, AsnEncodingRules.BER));
229+
}
230+
catch
231+
{
232+
return new EncodedSequence(trimmedPayload);
233+
}
218234
}
219-
catch
235+
236+
if (sequenceKind == SequenceKind.VarBindList)
220237
{
221238
try
222239
{
@@ -227,6 +244,8 @@ private static IAsnSerializable Parse(byte[] payload)
227244
return new EncodedSequence(trimmedPayload);
228245
}
229246
}
247+
248+
return new EncodedSequence(trimmedPayload);
230249
}
231250
}
232251
catch (Exception ex) when (ex is not SnmpException)
@@ -239,54 +258,110 @@ private static IAsnSerializable Parse(byte[] payload)
239258

240259
private sealed class EncodedSequence : IAsnSerializable
241260
{
242-
private readonly byte[] _encoded;
261+
private readonly ReadOnlyMemory<byte> _encoded;
243262

244-
public EncodedSequence(byte[] encoded)
263+
public EncodedSequence(ReadOnlyMemory<byte> encoded)
245264
{
246-
_encoded = encoded ?? throw new ArgumentNullException(nameof(encoded));
265+
_encoded = encoded;
247266
}
248267

249268
public SnmpType TypeCode => SnmpType.Sequence;
250269

251270
public void WriteTo(AsnWriter writer)
252271
{
253-
writer.WriteEncodedValue(_encoded);
272+
writer.WriteEncodedValue(_encoded.Span);
273+
}
274+
}
275+
276+
private enum SequenceKind
277+
{
278+
Unknown = 0,
279+
Scope = 1,
280+
VarBindList = 2,
281+
}
282+
283+
private static SequenceKind ClassifySequence(ReadOnlyMemory<byte> payload)
284+
{
285+
try
286+
{
287+
var reader = new AsnReader(payload, AsnEncodingRules.BER);
288+
var sequence = reader.ReadSequence();
289+
if (!sequence.HasData)
290+
{
291+
return SequenceKind.Unknown;
292+
}
293+
294+
var firstTag = sequence.PeekTag();
295+
if (firstTag.HasSameClassAndValue(Asn1Tag.Sequence))
296+
{
297+
return SequenceKind.VarBindList;
298+
}
299+
300+
if (!firstTag.HasSameClassAndValue(Asn1Tag.PrimitiveOctetString))
301+
{
302+
return SequenceKind.Unknown;
303+
}
304+
305+
// Scope starts with contextEngineId + contextName (octet strings),
306+
// followed by a context-specific constructed PDU.
307+
sequence.ReadOctetString();
308+
if (!sequence.HasData || !sequence.PeekTag().HasSameClassAndValue(Asn1Tag.PrimitiveOctetString))
309+
{
310+
return SequenceKind.Unknown;
311+
}
312+
313+
sequence.ReadOctetString();
314+
if (!sequence.HasData)
315+
{
316+
return SequenceKind.Unknown;
317+
}
318+
319+
var pduTag = sequence.PeekTag();
320+
return pduTag.TagClass == TagClass.ContextSpecific && pduTag.IsConstructed
321+
? SequenceKind.Scope
322+
: SequenceKind.Unknown;
323+
}
324+
catch
325+
{
326+
return SequenceKind.Unknown;
254327
}
255328
}
256329

257-
private static byte[] TrimToSingleBerValue(byte[] payload)
330+
private static ReadOnlyMemory<byte> TrimToSingleBerValue(ReadOnlyMemory<byte> payload)
258331
{
259-
if (payload.Length < 2)
332+
var span = payload.Span;
333+
334+
if (span.Length < 2)
260335
{
261336
throw new SnmpException("invalid BER data");
262337
}
263338

264339
var offset = 1; // tag octet
265340

266341
// High-tag-number form.
267-
if ((payload[0] & 0x1F) == 0x1F)
342+
if ((span[0] & 0x1F) == 0x1F)
268343
{
269344
while (true)
270345
{
271-
if (offset >= payload.Length)
346+
if (offset >= span.Length)
272347
{
273348
throw new SnmpException("invalid BER data");
274349
}
275350

276-
var octet = payload[offset++];
351+
var octet = span[offset++];
277352
if ((octet & 0x80) == 0)
278353
{
279354
break;
280355
}
281356
}
282357
}
283358

284-
if (offset >= payload.Length)
359+
if (offset >= span.Length)
285360
{
286361
throw new SnmpException("invalid BER data");
287362
}
288363

289-
var firstLengthOctet = payload[offset++];
364+
var firstLengthOctet = span[offset++];
290365
long contentLength;
291366
if ((firstLengthOctet & 0x80) == 0)
292367
{
@@ -295,25 +370,25 @@ private static byte[] TrimToSingleBerValue(byte[] payload)
295370
else
296371
{
297372
var lengthOctetCount = firstLengthOctet & 0x7F;
298-
if (lengthOctetCount == 0 || offset + lengthOctetCount > payload.Length)
373+
if (lengthOctetCount == 0 || offset + lengthOctetCount > span.Length)
299374
{
300375
throw new SnmpException("invalid BER length");
301376
}
302377

303378
contentLength = 0;
304379
for (var i = 0; i < lengthOctetCount; i++)
305380
{
306-
contentLength = (contentLength << 8) | payload[offset++];
381+
contentLength = (contentLength << 8) | span[offset++];
307382
}
308383
}
309384

310385
var totalLength = offset + contentLength;
311-
if (totalLength <= 0 || totalLength > payload.Length)
386+
if (totalLength <= 0 || totalLength > span.Length)
312387
{
313388
throw new SnmpException("invalid BER length");
314389
}
315390

316-
if (totalLength == payload.Length)
391+
if (totalLength == span.Length)
317392
{
318393
return payload;
319394
}
@@ -323,8 +398,6 @@ private static byte[] TrimToSingleBerValue(byte[] payload)
323398
throw new SnmpException("BER value too large");
324399
}
325400

326-
var trimmed = new byte[(int)totalLength];
327-
Buffer.BlockCopy(payload, 0, trimmed, 0, trimmed.Length);
328-
return trimmed;
401+
return payload.Slice(0, (int)totalLength);
329402
}
330403
}

SharpSnmpLib/Messaging/MessageFactory.cs

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,8 @@ public static IList<ISnmpMessage> ParseMessages(ReadOnlyMemory<byte> bytes, User
133133
// Continue parsing messages until we've consumed all data
134134
while (reader.HasData)
135135
{
136-
// Get the entire message data before we move on to the next one
137-
ReadOnlyMemory<byte> messageData = reader.PeekEncodedValue().ToArray();
138-
139-
// Skip over this message in the main reader so we can continue with the next message
140-
reader.ReadEncodedValue();
136+
// Read the full encoded message directly to avoid extra copy/allocation.
137+
ReadOnlyMemory<byte> messageData = reader.ReadEncodedValue();
141138

142139
// Use a temporary reader to peek at the version
143140
var versionReader = new AsnReader(messageData, AsnEncodingRules.BER);
@@ -213,11 +210,6 @@ public static IList<ISnmpMessage> ParseMessages(ReadOnlyMemory<byte> bytes, User
213210
return result;
214211
}
215212

216-
private static void ProcessV3Security(DotNetSnmp.Protocol.V3.SnmpV3Message v3Message, UserRegistry registry)
217-
{
218-
_ = ProcessV3Security(v3Message, registry, throwOnV3SecurityError: true);
219-
}
220-
221213
private static V3SecurityState ProcessV3Security(
222214
DotNetSnmp.Protocol.V3.SnmpV3Message v3Message,
223215
UserRegistry registry,
@@ -254,7 +246,7 @@ private static V3SecurityState ProcessV3Security(
254246
var auth = privacy.AuthenticationProvider;
255247

256248
// Process authentication if needed
257-
if (msgFlags.HasFlag(DotNetSnmp.Protocol.V3.Security.MsgFlag.Auth))
249+
if ((msgFlags & DotNetSnmp.Protocol.V3.Security.MsgFlag.Auth) != 0)
258250
{
259251
bool authenticated = auth.AuthenticateIncomingMsg(v3Message);
260252
if (!authenticated)
@@ -270,7 +262,7 @@ private static V3SecurityState ProcessV3Security(
270262
}
271263

272264
// Process privacy (decryption) if needed
273-
if (msgFlags.HasFlag(DotNetSnmp.Protocol.V3.Security.MsgFlag.Priv))
265+
if ((msgFlags & DotNetSnmp.Protocol.V3.Security.MsgFlag.Priv) != 0)
274266
{
275267
try
276268
{

SharpSnmpLib/V3/Security/Authentication/AuthenticationProviderBase.cs

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ public abstract class AuthenticationProviderBase : IAuthenticationProvider
2222
private readonly Func<byte[], HMAC> _create;
2323
private readonly ReadOnlyMemory<byte> _passcode;
2424
private readonly HashAlgorithmName _name;
25+
private readonly object _localizedKeyCacheLock = new();
26+
private readonly List<LocalizedKeyCacheEntry> _localizedKeyCache = [];
27+
private const int LocalizedKeyCacheCapacity = 64;
28+
29+
private sealed class LocalizedKeyCacheEntry
30+
{
31+
public required byte[] Secret { get; init; }
32+
33+
public required byte[] EngineId { get; init; }
34+
35+
public required byte[] LocalizedKey { get; init; }
36+
}
2537

2638
/// <summary>
2739
/// Initializes a new instance of AuthenticationProviderBase.
@@ -47,12 +59,59 @@ protected AuthenticationProviderBase(int digestSize, int truncatedDigestSize, Ha
4759
/// <returns>An HMAC algorithm instance initialized with the localized key.</returns>
4860
private HMAC CreateHmac(ReadOnlyMemory<byte> engineId)
4961
{
50-
// Use stackalloc for small keys, typically digest sizes are reasonable for stack allocation
51-
Span<byte> key = stackalloc byte[DigestSize];
52-
PasswordToKey(_passcode, engineId, key);
62+
// Localized keys are expensive to derive (RFC 3414 1MB passphrase expansion),
63+
// so cache them for this provider instance.
64+
var localizedKey = GetLocalizedKey(engineId);
65+
return _create(localizedKey);
66+
}
67+
68+
private byte[] GetLocalizedKey(ReadOnlyMemory<byte> engineId)
69+
=> GetLocalizedKey(_passcode, engineId);
70+
71+
private byte[] GetLocalizedKey(ReadOnlyMemory<byte> secret, ReadOnlyMemory<byte> engineId)
72+
{
73+
lock (_localizedKeyCacheLock)
74+
{
75+
foreach (var entry in _localizedKeyCache)
76+
{
77+
if (secret.Span.SequenceEqual(entry.Secret)
78+
&& engineId.Span.SequenceEqual(entry.EngineId))
79+
{
80+
return entry.LocalizedKey;
81+
}
82+
}
83+
}
84+
85+
var computed = new byte[DigestSize];
86+
ComputeLocalizedKey(secret, engineId, computed);
87+
var copiedSecret = secret.ToArray();
88+
var copiedEngineId = engineId.ToArray();
5389

54-
// Unfortunately, this allocation is unavoidable due to the HMAC API requiring a byte[]
55-
return _create(key.ToArray());
90+
lock (_localizedKeyCacheLock)
91+
{
92+
foreach (var entry in _localizedKeyCache)
93+
{
94+
if (copiedSecret.AsSpan().SequenceEqual(entry.Secret)
95+
&& copiedEngineId.AsSpan().SequenceEqual(entry.EngineId))
96+
{
97+
return entry.LocalizedKey;
98+
}
99+
}
100+
101+
if (_localizedKeyCache.Count >= LocalizedKeyCacheCapacity)
102+
{
103+
_localizedKeyCache.RemoveAt(0);
104+
}
105+
106+
_localizedKeyCache.Add(new LocalizedKeyCacheEntry
107+
{
108+
Secret = copiedSecret,
109+
EngineId = copiedEngineId,
110+
LocalizedKey = computed,
111+
});
112+
}
113+
114+
return computed;
56115
}
57116

58117
/// <inheritdoc/>
@@ -107,6 +166,25 @@ public bool AuthenticateIncomingMsg(SnmpV3Message message)
107166

108167
/// <inheritdoc/>
109168
public void PasswordToKey(in ReadOnlyMemory<byte> secret, in ReadOnlyMemory<byte> engineId, Span<byte> destination)
169+
{
170+
if (destination.Length < DigestSize)
171+
{
172+
throw new ArgumentException($"Destination is too small. Must be >= {DigestSize}. Current: {destination.Length}.", nameof(destination));
173+
}
174+
175+
// Common hot path: key localization into digest-sized buffer (auth/priv providers).
176+
// Reuse cached localized key instead of repeating RFC 3414 1MB expansion every call.
177+
if (destination.Length == DigestSize)
178+
{
179+
var localizedKey = GetLocalizedKey(secret, engineId);
180+
localizedKey.CopyTo(destination);
181+
return;
182+
}
183+
184+
ComputeLocalizedKey(secret, engineId, destination);
185+
}
186+
187+
private void ComputeLocalizedKey(in ReadOnlyMemory<byte> secret, in ReadOnlyMemory<byte> engineId, Span<byte> destination)
110188
{
111189
using var hash = IncrementalHash.CreateHash(_name);
112190
KeyUtils.GenerateLocalizedKey(secret.Span, engineId.Span, hash, destination);

0 commit comments

Comments
 (0)