ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
smb2.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file smb2.cpp
6 * @brief SMB2 client wire codec implementation (see smb2.h). All fields little-endian.
7 */
8
9#include "smb2.h"
10
11#if PC_ENABLE_SMB
12
13#include <string.h>
14
15#include "crypto/aead/aes128gcm.h" // pc_aes128gcm_* keyed AEAD (SMB 3.x AES-128-GCM)
16#include "crypto/aead/aesccm.h" // pc_aesccm_seal_tag/open_tag (SMB 3.x AES-128/256-CCM)
17#include "crypto/aead/aesgcm.h" // pc_aesgcm_* keyed AEAD (SMB 3.x AES-256-GCM)
18#include "crypto/hash/sha512.h" // pc_sha512 for the SMB 3.1.1 preauth-integrity chain
19#include "crypto/kdf/kdf.h" // pc_kdf_ctr_hmac_sha256 for SMB 3.x key derivation
20#include "crypto/mac/aes_cmac.h" // pc_aes_cmac for SMB 3.x message signing
21#include "crypto/mac/hmac_sha256.h" // pc_hmac_sha256 for SMB 2.x message signing
22#include "server/mmgr/secure.h" // SecureBorrow: the per-call GCM context
24
25static const uint8_t SMB2_PROTOCOL_ID[4] = {0xFE, 'S', 'M', 'B'};
26
27size_t pc_smb2_transport_frame(uint8_t *out, size_t cap, const uint8_t *msg, size_t msg_len)
28{
29 if (!out || !msg || msg_len > 0x00FFFFFF || 4 + msg_len > cap)
30 {
31 return 0;
32 }
33 out[0] = 0x00; // Direct TCP: first byte MUST be zero
34 out[1] = (uint8_t)(msg_len >> 16);
35 out[2] = (uint8_t)(msg_len >> 8);
36 out[3] = (uint8_t)(msg_len);
37 memcpy(out + 4, msg, msg_len);
38 return 4 + msg_len;
39}
40
41uint32_t pc_smb2_transport_len(const uint8_t *buf, size_t len)
42{
43 if (!buf || len < 4 || buf[0] != 0x00)
44 {
45 return 0;
46 }
47 return ((uint32_t)buf[1] << 16) | ((uint32_t)buf[2] << 8) | (uint32_t)buf[3];
48}
49
50size_t pc_smb2_build_header(uint8_t *buf, size_t cap, Smb2Command command, uint16_t credit_request, uint64_t message_id,
51 uint32_t tree_id, uint64_t session_id)
52{
53 if (!buf || cap < PC_SMB2_HEADER_SIZE)
54 {
55 return 0;
56 }
57 memset(buf, 0, PC_SMB2_HEADER_SIZE);
58 memcpy(buf + 0, SMB2_PROTOCOL_ID, 4); // ProtocolId
59 pc_wr16le(buf + 4, 64); // StructureSize
60 // bytes 6 CreditCharge and 8 Status/ChannelSequence stay zero
61 pc_wr16le(buf + 12, (uint16_t)command); // Command
62 pc_wr16le(buf + 14, credit_request); // CreditRequest
63 // byte 16 Flags stays zero for a client request; byte 20 NextCommand stays zero
64 pc_wr64le(buf + 24, message_id); // MessageId
65 // byte 32 Reserved stays zero
66 pc_wr32le(buf + 36, tree_id); // TreeId
67 pc_wr64le(buf + 40, session_id); // SessionId
68 // bytes 48 through 63 Signature stay zero
69 return PC_SMB2_HEADER_SIZE;
70}
71
72bool pc_smb2_parse_header(const uint8_t *buf, size_t len, Smb2Header *out)
73{
74 if (!buf || !out || len < PC_SMB2_HEADER_SIZE)
75 {
76 return false;
77 }
78 if (memcmp(buf, SMB2_PROTOCOL_ID, 4) != 0 || pc_rd16le(buf + 4) != 64)
79 {
80 return false;
81 }
82 out->status = pc_rd32le(buf + 8);
83 out->command = (Smb2Command)pc_rd16le(buf + 12);
84 out->credit_response = pc_rd16le(buf + 14);
85 out->flags = pc_rd32le(buf + 16);
86 out->message_id = pc_rd64le(buf + 24);
87 out->tree_id = pc_rd32le(buf + 36);
88 out->session_id = pc_rd64le(buf + 40);
89 return true;
90}
91
92size_t pc_smb2_build_negotiate(uint8_t *buf, size_t cap, const uint8_t client_guid[16], uint16_t security_mode)
93{
94 static const Smb2Dialect dialects[] = {Smb2Dialect::SMB2_DIALECT_0202, Smb2Dialect::SMB2_DIALECT_0210,
95 Smb2Dialect::SMB2_DIALECT_0300, Smb2Dialect::SMB2_DIALECT_0302};
96 const uint16_t ndialects = (uint16_t)(sizeof(dialects) / sizeof(dialects[0]));
97 const size_t total = PC_SMB2_HEADER_SIZE + 36 + (size_t)ndialects * 2; // header + fixed body + dialects
98 if (!buf || !client_guid || cap < total)
99 {
100 return 0;
101 }
102
103 // GCOVR_EXCL_START cap >= total (64 + 36 + 8) >= PC_SMB2_HEADER_SIZE was checked above, so this cannot fail
104 if (pc_smb2_build_header(buf, cap, Smb2Command::SMB2_NEGOTIATE, 1, 0, 0, 0) == 0)
105 {
106 return 0;
107 }
108 // GCOVR_EXCL_STOP
109
110 uint8_t *b = buf + PC_SMB2_HEADER_SIZE; // NEGOTIATE request body
111 memset(b, 0, 36);
112 pc_wr16le(b + 0, 36); // StructureSize
113 pc_wr16le(b + 2, ndialects); // DialectCount
114 pc_wr16le(b + 4, security_mode); // SecurityMode
115 // byte 6 Reserved and byte 8 Capabilities stay zero
116 memcpy(b + 12, client_guid, 16); // ClientGuid
117 // byte 28 ClientStartTime stays zero; only 3.1.1 reinterprets these 8 bytes as negotiate-context fields
118 for (uint16_t i = 0; i < ndialects; i++)
119 {
120 pc_wr16le(b + 36 + i * 2, (uint16_t)dialects[i]);
121 }
122 return total;
123}
124
125bool pc_smb2_parse_negotiate_response(const uint8_t *msg, size_t len, Smb2NegotiateResp *out)
126{
127 if (!msg || !out)
128 {
129 return false;
130 }
131 Smb2Header h;
132 if (!pc_smb2_parse_header(msg, len, &h) || h.command != Smb2Command::SMB2_NEGOTIATE)
133 {
134 return false;
135 }
136 // The fixed response body is 64 bytes (StructureSize .. NegotiateContextOffset), Buffer follows.
137 if (len < PC_SMB2_HEADER_SIZE + 64)
138 {
139 return false;
140 }
141 const uint8_t *b = msg + PC_SMB2_HEADER_SIZE;
142 if (pc_rd16le(b + 0) != 65) // StructureSize
143 {
144 return false;
145 }
146
147 out->security_mode = pc_rd16le(b + 2);
148 out->dialect = pc_rd16le(b + 4);
149 memcpy(out->server_guid, b + 8, 16);
150 out->capabilities = pc_rd32le(b + 24);
151 out->max_transact = pc_rd32le(b + 28);
152 out->max_read = pc_rd32le(b + 32);
153 out->max_write = pc_rd32le(b + 36);
154
155 uint16_t sec_off = pc_rd16le(b + 56); // SecurityBufferOffset - from the start of the SMB2 header (msg)
156 uint16_t sec_len = pc_rd16le(b + 58); // SecurityBufferLength
157 if (sec_len == 0)
158 {
159 out->sec_buf = nullptr;
160 out->sec_buf_len = 0;
161 return true;
162 }
163 if ((size_t)sec_off + sec_len > len || sec_off < PC_SMB2_HEADER_SIZE)
164 {
165 return false; // security buffer out of bounds - fail closed
166 }
167 out->sec_buf = msg + sec_off;
168 out->sec_buf_len = sec_len;
169 return true;
170}
171
172size_t pc_smb2_build_negotiate_311(uint8_t *buf, size_t cap, const uint8_t client_guid[16], uint16_t security_mode,
173 const uint8_t *salt, size_t salt_len, const uint16_t *ciphers, size_t cipher_count)
174{
175 static const Smb2Dialect dialects[] = {Smb2Dialect::SMB2_DIALECT_0202, Smb2Dialect::SMB2_DIALECT_0210,
176 Smb2Dialect::SMB2_DIALECT_0300, Smb2Dialect::SMB2_DIALECT_0302,
177 Smb2Dialect::SMB2_DIALECT_0311};
178 const uint16_t ndialects = (uint16_t)(sizeof(dialects) / sizeof(dialects[0]));
179 if (!buf || !client_guid || !salt || salt_len == 0 || salt_len > 0xFFFF ||
180 cipher_count > PC_SMB2_MAX_OFFER_CIPHERS || (cipher_count > 0 && !ciphers))
181 {
182 return 0;
183 }
184
185 // header(64) + fixed body(36) + dialects(2*n), padded to 8, then the negotiate-context list. Each
186 // context is ContextType(2) + DataLength(2) + Reserved(4) + Data, 8-byte aligned (MS-SMB2 §2.2.3.1).
187 const bool offer_enc = cipher_count > 0;
188 const size_t body_end = PC_SMB2_HEADER_SIZE + 36 + (size_t)ndialects * 2;
189 const size_t ctx_start = (body_end + 7) & ~(size_t)7; // NegotiateContextOffset (from msg start)
190 const size_t preauth_data = 6 + salt_len; // HashAlgorithmCount + SaltLength + 1 hash + Salt
191 const size_t preauth_ctx = 8 + preauth_data; // context header + data
192 const size_t after_preauth = ctx_start + preauth_ctx;
193 const size_t preauth_pad = ((after_preauth + 7) & ~(size_t)7) - after_preauth; // align the next context
194 const size_t sign_ctx = 8 + 4; // header + SigningAlgorithmCount + 1 algorithm
195 const size_t after_sign = ctx_start + preauth_ctx + preauth_pad + sign_ctx;
196 // Pad after signing only when an encryption context follows (it must be 8-byte aligned).
197 const size_t sign_pad = offer_enc ? (((after_sign + 7) & ~(size_t)7) - after_sign) : 0;
198 const size_t enc_ctx = offer_enc ? (8 + 2 + 2 * cipher_count) : 0; // header + CipherCount + Ciphers[]
199 const uint16_t ctx_count = offer_enc ? 3 : 2;
200 const size_t total = ctx_start + preauth_ctx + preauth_pad + sign_ctx + sign_pad + enc_ctx;
201 if (cap < total)
202 {
203 return 0;
204 }
205
206 // GCOVR_EXCL_START cap >= total >= PC_SMB2_HEADER_SIZE was checked above, so this cannot fail
207 if (pc_smb2_build_header(buf, cap, Smb2Command::SMB2_NEGOTIATE, 1, 0, 0, 0) == 0)
208 {
209 return 0;
210 }
211 // GCOVR_EXCL_STOP
212
213 uint8_t *b = buf + PC_SMB2_HEADER_SIZE;
214 memset(b, 0, ctx_start - PC_SMB2_HEADER_SIZE); // fixed body + dialects + alignment pad
215 pc_wr16le(b + 0, 36); // StructureSize
216 pc_wr16le(b + 2, ndialects); // DialectCount
217 pc_wr16le(b + 4, security_mode); // SecurityMode
218 pc_wr32le(b + 8, Smb2GlobalCap::SMB2_GLOBAL_CAP_ENCRYPTION); // Capabilities: advertise encryption support
219 memcpy(b + 12, client_guid, 16); // ClientGuid
220 pc_wr32le(b + 28, (uint32_t)ctx_start); // NegotiateContextOffset (overlays ClientStartTime)
221 pc_wr16le(b + 32, ctx_count); // NegotiateContextCount (preauth + signing [+ encryption])
222 for (uint16_t i = 0; i < ndialects; i++)
223 {
224 pc_wr16le(b + 36 + i * 2, (uint16_t)dialects[i]);
225 }
226
227 // Context 1 - PREAUTH_INTEGRITY_CAPABILITIES (mandatory once 0x0311 is offered), §2.2.3.1.1.
228 uint8_t *c = buf + ctx_start;
229 pc_wr16le(c + 0, Smb2NegotiateContextType::SMB2_PREAUTH_INTEGRITY_CAPABILITIES);
230 pc_wr16le(c + 2, (uint16_t)preauth_data);
231 pc_wr32le(c + 4, 0); // Reserved
232 pc_wr16le(c + 8, 1); // HashAlgorithmCount
233 pc_wr16le(c + 10, (uint16_t)salt_len); // SaltLength
234 pc_wr16le(c + 12, Smb2HashAlgorithm::SMB2_PREAUTH_INTEGRITY_SHA512); // HashAlgorithms[0]
235 memcpy(c + 14, salt, salt_len); // Salt
236
237 // Context 2 - SIGNING_CAPABILITIES advertising AES-CMAC (the algorithm this client signs 3.x with;
238 // the mandatory-to-implement SMB 3.x signer and the Windows default). A 3.1.1 server that accepts it
239 // signs with AES-128-CMAC over the KDF-derived key; 3.0 / 3.0.2 use AES-CMAC unconditionally.
240 uint8_t *c2 = c + preauth_ctx + preauth_pad;
241 if (preauth_pad)
242 {
243 memset(c + preauth_ctx, 0, preauth_pad);
244 }
245 pc_wr16le(c2 + 0, Smb2NegotiateContextType::SMB2_SIGNING_CAPABILITIES);
246 pc_wr16le(c2 + 2, 4); // DataLength
247 pc_wr32le(c2 + 4, 0); // Reserved
248 pc_wr16le(c2 + 8, 1); // SigningAlgorithmCount
249 pc_wr16le(c2 + 10, Smb2SigningAlgorithm::SMB2_SIGNING_AES_CMAC); // SigningAlgorithms[0]
250
251 // Context 3 - ENCRYPTION_CAPABILITIES listing the offered ciphers in preference order (§2.2.3.1.2). A
252 // server that accepts one may require an encrypted session/share; we then wrap messages in a
253 // TRANSFORM_HEADER (pc_smb2_encrypt). All four SMB 3.1.1 ciphers can be offered.
254 if (offer_enc)
255 {
256 uint8_t *c3 = c2 + sign_ctx + sign_pad;
257 if (sign_pad)
258 {
259 memset(c2 + sign_ctx, 0, sign_pad);
260 }
261 pc_wr16le(c3 + 0, Smb2NegotiateContextType::SMB2_ENCRYPTION_CAPABILITIES);
262 pc_wr16le(c3 + 2, (uint16_t)(2 + 2 * cipher_count)); // DataLength = CipherCount(2) + Ciphers[]
263 pc_wr32le(c3 + 4, 0); // Reserved
264 pc_wr16le(c3 + 8, (uint16_t)cipher_count); // CipherCount
265 for (size_t i = 0; i < cipher_count; i++)
266 {
267 pc_wr16le(c3 + 10 + i * 2, ciphers[i]); // Ciphers[i]
268 }
269 }
270 return total;
271}
272
273bool pc_smb2_parse_negotiate_contexts(const uint8_t *msg, size_t len, Smb2NegotiateContexts *out)
274{
275 if (!msg || !out)
276 {
277 return false;
278 }
279 memset(out, 0, sizeof(*out));
280 Smb2Header h;
281 if (!pc_smb2_parse_header(msg, len, &h) || h.command != Smb2Command::SMB2_NEGOTIATE)
282 {
283 return false;
284 }
285 if (len < PC_SMB2_HEADER_SIZE + 64)
286 {
287 return false;
288 }
289 const uint8_t *b = msg + PC_SMB2_HEADER_SIZE;
290 if (pc_rd16le(b + 0) != 65) // StructureSize
291 {
292 return false;
293 }
294 uint16_t count = pc_rd16le(b + 6); // NegotiateContextCount (reserved for < 3.1.1)
295 uint32_t off = pc_rd32le(b + 60); // NegotiateContextOffset (from the SMB2 header start)
296 if (count == 0 || off < PC_SMB2_HEADER_SIZE)
297 {
298 return false; // not a 3.1.1 response carrying a context list
299 }
300
301 size_t p = off;
302 for (uint16_t i = 0; i < count; i++)
303 {
304 p = (p + 7) & ~(size_t)7; // every context is 8-byte aligned
305 if (p + 8 > len)
306 {
307 return false;
308 }
309 uint16_t ctype = pc_rd16le(msg + p);
310 uint16_t dlen = pc_rd16le(msg + p + 2); // Reserved (4) follows at p+4
311 size_t data = p + 8;
312 if (data + dlen > len)
313 {
314 return false;
315 }
316 const uint8_t *d = msg + data;
317 if (ctype == Smb2NegotiateContextType::SMB2_PREAUTH_INTEGRITY_CAPABILITIES && dlen >= 6)
318 {
319 uint16_t hcount = pc_rd16le(d + 0); // HashAlgorithmCount
320 uint16_t slen = pc_rd16le(d + 2); // SaltLength
321 if (hcount >= 1 && (size_t)dlen >= 4 + (size_t)hcount * 2 + slen)
322 {
323 out->have_preauth = true;
324 out->hash_algorithm = pc_rd16le(d + 4); // HashAlgorithms[0]
325 out->salt_len = slen;
326 out->salt = slen ? d + 4 + (size_t)hcount * 2 : nullptr;
327 }
328 }
329 else if (ctype == Smb2NegotiateContextType::SMB2_SIGNING_CAPABILITIES && dlen >= 4)
330 {
331 uint16_t scount = pc_rd16le(d + 0); // SigningAlgorithmCount
332 if (scount >= 1 && (size_t)dlen >= 2 + (size_t)scount * 2)
333 {
334 out->have_signing = true;
335 out->signing_algorithm = pc_rd16le(d + 2); // SigningAlgorithms[0]
336 }
337 }
338 else if (ctype == Smb2NegotiateContextType::SMB2_ENCRYPTION_CAPABILITIES && dlen >= 4)
339 {
340 uint16_t ccount = pc_rd16le(d + 0); // CipherCount
341 if (ccount >= 1 && (size_t)dlen >= 2 + (size_t)ccount * 2)
342 {
343 out->have_encryption = true;
344 out->cipher = pc_rd16le(d + 2); // Ciphers[0]
345 }
346 }
347 p = data + dlen;
348 }
349 return true;
350}
351
352void pc_smb_preauth_init(SmbPreauth *p)
353{
354 if (p)
355 {
356 memset(p->hash, 0, sizeof(p->hash)); // the preauth hash starts as 64 zero bytes (MS-SMB2 §3.1.5.2)
357 }
358}
359
360void pc_smb_preauth_update(SmbPreauth *p, const uint8_t *msg, size_t len)
361{
362 if (!p || (!msg && len))
363 {
364 return;
365 }
366 // hash = SHA-512(previous hash || message); chain the current value with the next handshake message.
367 // Snapshot the previous hash so the digest input never aliases its output buffer.
368 uint8_t prev[PC_SMB2_PREAUTH_HASH_LEN];
369 memcpy(prev, p->hash, sizeof(prev));
371 pc_sha512_init(&c);
372 pc_sha512_update(&c, prev, sizeof(prev));
373 pc_sha512_update(&c, msg, len);
374 pc_sha512_final(&c, p->hash);
375}
376
377size_t pc_smb2_build_session_setup(uint8_t *buf, size_t cap, uint64_t message_id, uint64_t session_id,
378 uint8_t security_mode, const uint8_t *sec_buf, size_t sec_len)
379{
380 const size_t body = 24; // fixed SESSION_SETUP request body (§2.2.5)
381 const size_t total = PC_SMB2_HEADER_SIZE + body + sec_len;
382 if (!buf || !sec_buf || sec_len == 0 || sec_len > 0xFFFF || cap < total)
383 {
384 return 0;
385 }
386 // GCOVR_EXCL_START cap >= total (64 + 24 + sec_len) >= PC_SMB2_HEADER_SIZE was checked above, so this cannot fail
387 if (pc_smb2_build_header(buf, cap, Smb2Command::SMB2_SESSION_SETUP, 1, message_id, 0, session_id) == 0)
388 {
389 return 0;
390 }
391 // GCOVR_EXCL_STOP
392
393 uint8_t *b = buf + PC_SMB2_HEADER_SIZE;
394 memset(b, 0, body);
395 pc_wr16le(b + 0, 25); // StructureSize (fixed 24 + 1 for the variable buffer)
396 b[2] = 0; // Flags (SMB2_SESSION_FLAG_BINDING only for 3.x channel binding)
397 b[3] = security_mode; // SecurityMode (one byte here)
398 // byte 4 Capabilities and byte 8 Channel stay zero
399 pc_wr16le(b + 12, (uint16_t)(PC_SMB2_HEADER_SIZE + body)); // SecurityBufferOffset (from the header start)
400 pc_wr16le(b + 14, (uint16_t)sec_len); // SecurityBufferLength
401 // byte 16 PreviousSessionId stays zero for a fresh session
402 memcpy(b + body, sec_buf, sec_len);
403 return total;
404}
405
406bool pc_smb2_parse_session_setup_response(const uint8_t *msg, size_t len, Smb2SessionSetupResp *out)
407{
408 if (!msg || !out)
409 {
410 return false;
411 }
412 Smb2Header h;
413 if (!pc_smb2_parse_header(msg, len, &h) || h.command != Smb2Command::SMB2_SESSION_SETUP)
414 {
415 return false;
416 }
417 // The fixed response body is 8 bytes (StructureSize .. SecurityBufferLength), Buffer follows.
418 if (len < PC_SMB2_HEADER_SIZE + 8)
419 {
420 return false;
421 }
422 const uint8_t *b = msg + PC_SMB2_HEADER_SIZE;
423 if (pc_rd16le(b + 0) != 9) // StructureSize
424 {
425 return false;
426 }
427
428 out->session_flags = pc_rd16le(b + 2);
429 uint16_t sec_off = pc_rd16le(b + 4); // SecurityBufferOffset - from the start of the SMB2 header (msg)
430 uint16_t sec_len = pc_rd16le(b + 6); // SecurityBufferLength
431 if (sec_len == 0)
432 {
433 out->sec_buf = nullptr;
434 out->sec_buf_len = 0;
435 return true;
436 }
437 if ((size_t)sec_off + sec_len > len || sec_off < PC_SMB2_HEADER_SIZE)
438 {
439 return false; // security buffer out of bounds - fail closed
440 }
441 out->sec_buf = msg + sec_off;
442 out->sec_buf_len = sec_len;
443 return true;
444}
445
446size_t pc_smb2_build_tree_connect(uint8_t *buf, size_t cap, uint64_t message_id, uint64_t session_id,
447 const uint8_t *path_utf16, size_t path_len)
448{
449 const size_t body = 8; // fixed TREE_CONNECT request body (§2.2.9)
450 const size_t total = PC_SMB2_HEADER_SIZE + body + path_len;
451 if (!buf || !path_utf16 || path_len == 0 || path_len > 0xFFFF || cap < total)
452 {
453 return 0;
454 }
455 // GCOVR_EXCL_START cap >= total (64 + 8 + path_len) >= PC_SMB2_HEADER_SIZE was checked above, so this cannot fail
456 if (pc_smb2_build_header(buf, cap, Smb2Command::SMB2_TREE_CONNECT, 1, message_id, 0, session_id) == 0)
457 {
458 return 0;
459 }
460 // GCOVR_EXCL_STOP
461
462 uint8_t *b = buf + PC_SMB2_HEADER_SIZE;
463 memset(b, 0, body);
464 pc_wr16le(b + 0, 9); // StructureSize
465 // byte 2 Flags/Reserved stays zero
466 pc_wr16le(b + 4, (uint16_t)(PC_SMB2_HEADER_SIZE + body)); // PathOffset (from the header start) = 72
467 pc_wr16le(b + 6, (uint16_t)path_len); // PathLength
468 memcpy(b + body, path_utf16, path_len); // the \\server\share path (UTF-16LE)
469 return total;
470}
471
472bool pc_smb2_parse_tree_connect_response(const uint8_t *msg, size_t len, Smb2TreeConnectResp *out)
473{
474 if (!msg || !out)
475 {
476 return false;
477 }
478 Smb2Header h;
479 if (!pc_smb2_parse_header(msg, len, &h) || h.command != Smb2Command::SMB2_TREE_CONNECT)
480 {
481 return false;
482 }
483 if (len < PC_SMB2_HEADER_SIZE + 16) // fixed 16-byte body, no variable buffer
484 {
485 return false;
486 }
487 const uint8_t *b = msg + PC_SMB2_HEADER_SIZE;
488 if (pc_rd16le(b + 0) != 16) // StructureSize
489 {
490 return false;
491 }
492 out->share_type = b[2];
493 out->share_flags = pc_rd32le(b + 4);
494 out->capabilities = pc_rd32le(b + 8);
495 out->maximal_access = pc_rd32le(b + 12);
496 return true;
497}
498
499size_t pc_smb2_build_create(uint8_t *buf, size_t cap, uint64_t message_id, uint64_t session_id, uint32_t tree_id,
500 uint32_t desired_access, uint32_t share_access, uint32_t create_disposition,
501 uint32_t create_options, const uint8_t *name_utf16, size_t name_len)
502{
503 const size_t body = 56; // fixed CREATE request body (§2.2.13)
504 const size_t total = PC_SMB2_HEADER_SIZE + body + name_len;
505 if (!buf || !name_utf16 || name_len == 0 || name_len > 0xFFFF || cap < total)
506 {
507 return 0;
508 }
509 // GCOVR_EXCL_START cap >= total (64 + 56 + name_len) >= PC_SMB2_HEADER_SIZE was checked above, so this cannot fail
510 if (pc_smb2_build_header(buf, cap, Smb2Command::SMB2_CREATE, 1, message_id, tree_id, session_id) == 0)
511 {
512 return 0;
513 }
514 // GCOVR_EXCL_STOP
515
516 uint8_t *b = buf + PC_SMB2_HEADER_SIZE;
517 memset(b, 0, body);
518 pc_wr16le(b + 0, 57); // StructureSize (fixed 56 + 1 for the variable buffer)
519 // byte 2 SecurityFlags and byte 3 RequestedOplockLevel stay zero (SMB2_OPLOCK_LEVEL_NONE)
520 pc_wr32le(b + 4, 2); // ImpersonationLevel = Impersonation
521 // byte 8 SmbCreateFlags and byte 16 Reserved stay zero
522 pc_wr32le(b + 24, desired_access); // DesiredAccess
523 pc_wr32le(b + 28, 0); // FileAttributes = 0
524 pc_wr32le(b + 32, share_access); // ShareAccess
525 pc_wr32le(b + 36, create_disposition); // CreateDisposition
526 pc_wr32le(b + 40, create_options); // CreateOptions
527 pc_wr16le(b + 44, (uint16_t)(PC_SMB2_HEADER_SIZE + body)); // NameOffset (from the header start) = 120
528 pc_wr16le(b + 46, (uint16_t)name_len); // NameLength
529 // bytes 48 CreateContextsOffset and 52 CreateContextsLength stay zero
530 memcpy(b + body, name_utf16, name_len);
531 return total;
532}
533
534bool pc_smb2_parse_create_response(const uint8_t *msg, size_t len, Smb2CreateResp *out)
535{
536 if (!msg || !out)
537 {
538 return false;
539 }
540 Smb2Header h;
541 if (!pc_smb2_parse_header(msg, len, &h) || h.command != Smb2Command::SMB2_CREATE)
542 {
543 return false;
544 }
545 if (len < PC_SMB2_HEADER_SIZE + 88) // fixed 88-byte body (StructureSize .. CreateContextsLength)
546 {
547 return false;
548 }
549 const uint8_t *b = msg + PC_SMB2_HEADER_SIZE;
550 if (pc_rd16le(b + 0) != 89) // StructureSize
551 {
552 return false;
553 }
554 out->create_action = pc_rd32le(b + 4);
555 out->end_of_file = pc_rd64le(b + 48);
556 out->file_attributes = pc_rd32le(b + 56);
557 memcpy(out->file_id, b + 64, 16); // FileId (persistent 8 + volatile 8)
558 return true;
559}
560
561size_t pc_smb2_build_close(uint8_t *buf, size_t cap, uint64_t message_id, uint64_t session_id, uint32_t tree_id,
562 const uint8_t file_id[16])
563{
564 const size_t body = 24; // fixed CLOSE request body (§2.2.15), no variable buffer
565 const size_t total = PC_SMB2_HEADER_SIZE + body;
566 if (!buf || !file_id || cap < total)
567 {
568 return 0;
569 }
570 // GCOVR_EXCL_START cap >= total (64 + 24) >= PC_SMB2_HEADER_SIZE was checked above, so this cannot fail
571 if (pc_smb2_build_header(buf, cap, Smb2Command::SMB2_CLOSE, 1, message_id, tree_id, session_id) == 0)
572 {
573 return 0;
574 }
575 // GCOVR_EXCL_STOP
576
577 uint8_t *b = buf + PC_SMB2_HEADER_SIZE;
578 memset(b, 0, body);
579 pc_wr16le(b + 0, 24); // StructureSize
580 // byte 2 Flags stays zero (no POSTQUERY_ATTRIB); byte 4 Reserved stays zero
581 memcpy(b + 8, file_id, 16); // FileId
582 return total;
583}
584
585bool pc_smb2_parse_close_response(const uint8_t *msg, size_t len, Smb2CloseResp *out)
586{
587 if (!msg || !out)
588 {
589 return false;
590 }
591 Smb2Header h;
592 if (!pc_smb2_parse_header(msg, len, &h) || h.command != Smb2Command::SMB2_CLOSE)
593 {
594 return false;
595 }
596 if (len < PC_SMB2_HEADER_SIZE + 60) // fixed 60-byte body
597 {
598 return false;
599 }
600 const uint8_t *b = msg + PC_SMB2_HEADER_SIZE;
601 if (pc_rd16le(b + 0) != 60) // StructureSize
602 {
603 return false;
604 }
605 out->end_of_file = pc_rd64le(b + 48);
606 out->file_attributes = pc_rd32le(b + 56);
607 return true;
608}
609
610size_t pc_smb2_build_read(uint8_t *buf, size_t cap, uint64_t message_id, uint64_t session_id, uint32_t tree_id,
611 const uint8_t file_id[16], uint32_t length, uint64_t offset)
612{
613 const size_t body = 48; // fixed READ request body (§2.2.19)
614 const size_t total = PC_SMB2_HEADER_SIZE + body + 1; // + a 1-byte buffer (StructureSize 49 convention)
615 if (!buf || !file_id || cap < total)
616 {
617 return 0;
618 }
619 // GCOVR_EXCL_START cap >= total (64 + 48 + 1) >= PC_SMB2_HEADER_SIZE was checked above, so this cannot fail
620 if (pc_smb2_build_header(buf, cap, Smb2Command::SMB2_READ, 1, message_id, tree_id, session_id) == 0)
621 {
622 return 0;
623 }
624 // GCOVR_EXCL_STOP
625
626 uint8_t *b = buf + PC_SMB2_HEADER_SIZE;
627 memset(b, 0, body + 1);
628 pc_wr16le(b + 0, 49); // StructureSize
629 b[2] =
630 (uint8_t)(PC_SMB2_HEADER_SIZE + 16); // Padding: requested data offset in the response (header + 16-byte body)
631 // byte 3 Flags stays zero
632 pc_wr32le(b + 4, length); // Length
633 pc_wr64le(b + 8, offset); // Offset
634 memcpy(b + 16, file_id, 16); // FileId
635 pc_wr32le(b + 32, 1); // MinimumCount = 1 (fail if the server returns nothing)
636 // bytes 36 Channel, 40 RemainingBytes and 44/46 ReadChannelInfoOffset/Length stay zero
637 // the one-byte Buffer at b+48 stays zero (already zeroed)
638 return total;
639}
640
641bool pc_smb2_parse_read_response(const uint8_t *msg, size_t len, Smb2ReadResp *out)
642{
643 if (!msg || !out)
644 {
645 return false;
646 }
647 Smb2Header h;
648 if (!pc_smb2_parse_header(msg, len, &h) || h.command != Smb2Command::SMB2_READ)
649 {
650 return false;
651 }
652 if (len < PC_SMB2_HEADER_SIZE + 16) // fixed 16-byte body (StructureSize .. Reserved2), Buffer follows
653 {
654 return false;
655 }
656 const uint8_t *b = msg + PC_SMB2_HEADER_SIZE;
657 if (pc_rd16le(b + 0) != 17) // StructureSize
658 {
659 return false;
660 }
661
662 uint8_t data_off = b[2]; // DataOffset - from the start of the SMB2 header (msg)
663 uint32_t data_len = pc_rd32le(b + 4); // DataLength
664 if (data_len == 0)
665 {
666 out->data = nullptr;
667 out->data_len = 0;
668 return true;
669 }
670 if (data_off < PC_SMB2_HEADER_SIZE || (size_t)data_off + data_len > len)
671 {
672 return false; // data out of bounds - fail closed
673 }
674 out->data = msg + data_off;
675 out->data_len = data_len;
676 return true;
677}
678
679size_t pc_smb2_build_write(uint8_t *buf, size_t cap, uint64_t message_id, uint64_t session_id, uint32_t tree_id,
680 const uint8_t file_id[16], const uint8_t *data, size_t data_len, uint64_t offset)
681{
682 const size_t body = 48; // fixed WRITE request body (§2.2.21)
683 const size_t total = PC_SMB2_HEADER_SIZE + body + data_len;
684 if (!buf || !file_id || !data || data_len == 0 || data_len > 0xFFFFFFFF || cap < total)
685 {
686 return 0;
687 }
688 // GCOVR_EXCL_START cap >= total (64 + 48 + data_len) >= PC_SMB2_HEADER_SIZE was checked above, so this cannot fail
689 if (pc_smb2_build_header(buf, cap, Smb2Command::SMB2_WRITE, 1, message_id, tree_id, session_id) == 0)
690 {
691 return 0;
692 }
693 // GCOVR_EXCL_STOP
694
695 uint8_t *b = buf + PC_SMB2_HEADER_SIZE;
696 memset(b, 0, body);
697 pc_wr16le(b + 0, 49); // StructureSize
698 pc_wr16le(b + 2, (uint16_t)(PC_SMB2_HEADER_SIZE + body)); // DataOffset (from the header start) = 112
699 pc_wr32le(b + 4, (uint32_t)data_len); // Length
700 pc_wr64le(b + 8, offset); // Offset
701 memcpy(b + 16, file_id, 16); // FileId
702 // bytes 32 Channel, 36 RemainingBytes, 40/42 WriteChannelInfoOffset/Length and 44 Flags stay zero
703 memcpy(b + body, data, data_len); // the data to write
704 return total;
705}
706
707bool pc_smb2_parse_write_response(const uint8_t *msg, size_t len, Smb2WriteResp *out)
708{
709 if (!msg || !out)
710 {
711 return false;
712 }
713 Smb2Header h;
714 if (!pc_smb2_parse_header(msg, len, &h) || h.command != Smb2Command::SMB2_WRITE)
715 {
716 return false;
717 }
718 if (len < PC_SMB2_HEADER_SIZE + 16) // fixed 16-byte body
719 {
720 return false;
721 }
722 const uint8_t *b = msg + PC_SMB2_HEADER_SIZE;
723 if (pc_rd16le(b + 0) != 17) // StructureSize
724 {
725 return false;
726 }
727 out->count = pc_rd32le(b + 4); // Count (bytes written)
728 return true;
729}
730
731// --- Message signing (MS-SMB2 §3.1.4.1 / §3.1.5.1) -------------------------
732// The SMB2 sync header places the Flags field at offset 16 (LE u32) and the 16-byte Signature at
733// offset 48. The MAC covers the whole message with the Signature zeroed. SMB 2.x uses HMAC-SHA256 (the
734// on-wire Signature is its first 16 octets); SMB 3.x uses AES-128-CMAC (its full 16-octet tag). The
735// FLAGS_SIGNED / zero-Signature / MAC / write-back framing is identical - only the MAC differs.
736#define PC_SMB2_FLAGS_OFF 16
737#define PC_SMB2_SIGNATURE_OFF 48
738#define PC_SMB2_SIGNATURE_LEN 16
739
740// A MAC that writes its first 16 octets into out16 (HMAC-SHA256 truncated, or AES-CMAC whole tag).
741typedef void (*Smb2MacFn)(const uint8_t key[16], const uint8_t *msg, size_t len, uint8_t out16[16]);
742
743static void mac_hmac_sha256(const uint8_t key[16], const uint8_t *msg, size_t len, uint8_t out16[16])
744{
745 uint8_t mac[32];
746 pc_hmac_sha256(key, 16, msg, len, mac);
747 memcpy(out16, mac, 16); // Signature = first 16 octets of the HMAC
748}
749
750static void mac_aes_cmac(const uint8_t key[16], const uint8_t *msg, size_t len, uint8_t out16[16])
751{
752 pc_aes_cmac(key, msg, len, out16); // the whole 16-octet CMAC tag
753}
754
755static void smb2_sign_framed(const uint8_t key[16], uint8_t *msg, size_t msg_len, Smb2MacFn mac)
756{
757 if (!key || !msg || msg_len < PC_SMB2_HEADER_SIZE)
758 {
759 return;
760 }
761 pc_wr32le(msg + PC_SMB2_FLAGS_OFF, pc_rd32le(msg + PC_SMB2_FLAGS_OFF) | Smb2HeaderFlags::SMB2_FLAGS_SIGNED);
762 memset(msg + PC_SMB2_SIGNATURE_OFF, 0, PC_SMB2_SIGNATURE_LEN); // zero the Signature before the MAC
763 uint8_t tag[PC_SMB2_SIGNATURE_LEN];
764 mac(key, msg, msg_len, tag);
765 memcpy(msg + PC_SMB2_SIGNATURE_OFF, tag, PC_SMB2_SIGNATURE_LEN);
766}
767
768static bool smb2_verify_framed(const uint8_t key[16], uint8_t *msg, size_t msg_len, Smb2MacFn mac)
769{
770 if (!key || !msg || msg_len < PC_SMB2_HEADER_SIZE)
771 {
772 return false;
773 }
774 uint8_t received[PC_SMB2_SIGNATURE_LEN];
775 memcpy(received, msg + PC_SMB2_SIGNATURE_OFF, PC_SMB2_SIGNATURE_LEN);
776 memset(msg + PC_SMB2_SIGNATURE_OFF, 0, PC_SMB2_SIGNATURE_LEN);
777 uint8_t tag[PC_SMB2_SIGNATURE_LEN];
778 mac(key, msg, msg_len, tag);
779 memcpy(msg + PC_SMB2_SIGNATURE_OFF, received, PC_SMB2_SIGNATURE_LEN); // restore; the message is unchanged
780 uint8_t diff = 0;
781 for (size_t i = 0; i < PC_SMB2_SIGNATURE_LEN; i++)
782 {
783 diff |= (uint8_t)(tag[i] ^ received[i]); // constant-time compare (no early exit)
784 }
785 return diff == 0;
786}
787
788void pc_smb2_sign(const uint8_t key[16], uint8_t *msg, size_t msg_len)
789{
790 smb2_sign_framed(key, msg, msg_len, mac_hmac_sha256);
791}
792
793bool pc_smb2_verify(const uint8_t key[16], uint8_t *msg, size_t msg_len)
794{
795 return smb2_verify_framed(key, msg, msg_len, mac_hmac_sha256);
796}
797
798void pc_smb2_sign_cmac(const uint8_t key[16], uint8_t *msg, size_t msg_len)
799{
800 smb2_sign_framed(key, msg, msg_len, mac_aes_cmac);
801}
802
803bool pc_smb2_verify_cmac(const uint8_t key[16], uint8_t *msg, size_t msg_len)
804{
805 return smb2_verify_framed(key, msg, msg_len, mac_aes_cmac);
806}
807
808bool pc_smb3_derive_signing_key(const uint8_t session_key[16], uint16_t dialect, const uint8_t *preauth,
809 uint8_t out_key[16])
810{
811 if (!session_key || !out_key)
812 {
813 return false;
814 }
815
816 // Assemble the SP800-108 fixed input: `Label || 0x00 || Context || [L]`. The KDF prepends the 32-bit
817 // counter. Each label literal carries its own trailing NUL (sizeof includes it), which IS the Label;
818 // the explicit 0x00 after it is the KDF's Label||Context separator - so the label is followed by two
819 // NULs on the wire, matching Windows / Samba / impacket (verified vs impacket KDF_CounterMode).
820 uint8_t fixed[96];
821 size_t n = 0;
822 if (dialect == (uint16_t)Smb2Dialect::SMB2_DIALECT_0311)
823 {
824 if (!preauth)
825 {
826 return false;
827 }
828 static const char label[] = "SMBSigningKey"; // sizeof == 14 ("SMBSigningKey\0")
829 memcpy(fixed + n, label, sizeof(label));
830 n += sizeof(label);
831 fixed[n++] = 0x00; // Label||0x00 separator
832 memcpy(fixed + n, preauth, 64); // Context = the 64-byte preauth-integrity hash
833 n += 64;
834 }
835 else
836 {
837 static const char label[] = "SMB2AESCMAC"; // sizeof == 12 ("SMB2AESCMAC\0")
838 static const char context[] = "SmbSign"; // sizeof == 8 ("SmbSign\0")
839 memcpy(fixed + n, label, sizeof(label));
840 n += sizeof(label);
841 fixed[n++] = 0x00; // separator
842 memcpy(fixed + n, context, sizeof(context));
843 n += sizeof(context);
844 }
845 // [L] = the derived-key length in bits (128), 32-bit big-endian.
846 fixed[n++] = 0x00;
847 fixed[n++] = 0x00;
848 fixed[n++] = 0x00;
849 fixed[n++] = 0x80;
850 return pc_kdf_ctr_hmac_sha256(session_key, 16, fixed, n, out_key, 16);
851}
852
853// One SMB 3.x cipher key (MS-SMB2 §3.1.4.2), same SP800-108 construction as the signing key. @p c2s picks the
854// client->server (encrypt) label/context vs server->client (decrypt). 3.1.1 keys the context on the preauth
855// hash; 3.0/3.0.2 use the fixed "ServerIn "/"ServerOut" contexts (label "SMB2AESCCM" even when the cipher is
856// GCM, per the spec).
857static bool smb3_derive_cipher_key(const uint8_t session_key[16], uint16_t dialect, const uint8_t *preauth, bool c2s,
858 size_t key_len, uint8_t *out_key)
859{
860 uint8_t fixed[96];
861 size_t n = 0;
862 if (dialect == (uint16_t)Smb2Dialect::SMB2_DIALECT_0311)
863 {
864 if (!preauth)
865 {
866 return false;
867 }
868 static const char c2s_label[] = "SMBC2SCipherKey"; // sizeof == 16 (15 chars + NUL)
869 static const char s2c_label[] = "SMBS2CCipherKey";
870 memcpy(fixed + n, c2s ? c2s_label : s2c_label, sizeof(c2s_label));
871 n += sizeof(c2s_label);
872 fixed[n++] = 0x00; // Label||0x00 separator
873 memcpy(fixed + n, preauth, 64); // Context = the 64-byte preauth-integrity hash
874 n += 64;
875 }
876 else
877 {
878 static const char label[] = "SMB2AESCCM"; // sizeof == 11
879 static const char ctx_in[] = "ServerIn "; // sizeof == 10 (note the trailing space, MS-SMB2 §3.1.4.2)
880 static const char ctx_out[] = "ServerOut"; // sizeof == 10
881 memcpy(fixed + n, label, sizeof(label));
882 n += sizeof(label);
883 fixed[n++] = 0x00;
884 memcpy(fixed + n, c2s ? ctx_in : ctx_out, sizeof(ctx_in));
885 n += sizeof(ctx_in);
886 }
887 const uint32_t l_bits = (uint32_t)(key_len * 8); // [L] = key length in bits, 32-bit big-endian
888 fixed[n++] = (uint8_t)((l_bits >> 24) & 0xff);
889 fixed[n++] = (uint8_t)((l_bits >> 16) & 0xff);
890 fixed[n++] = (uint8_t)((l_bits >> 8) & 0xff);
891 fixed[n++] = (uint8_t)(l_bits & 0xff);
892 return pc_kdf_ctr_hmac_sha256(session_key, 16, fixed, n, out_key, key_len);
893}
894
895bool pc_smb3_derive_encryption_keys(const uint8_t session_key[16], uint16_t dialect, const uint8_t *preauth,
896 size_t key_len, uint8_t *out_c2s, uint8_t *out_s2c)
897{
898 if (!session_key || !out_c2s || !out_s2c || (key_len != 16 && key_len != 32))
899 {
900 return false;
901 }
902 return smb3_derive_cipher_key(session_key, dialect, preauth, true, key_len, out_c2s) &&
903 smb3_derive_cipher_key(session_key, dialect, preauth, false, key_len, out_s2c);
904}
905
906size_t pc_smb2_encrypt(uint16_t cipher, const uint8_t *key, const uint8_t nonce[PC_SMB2_NONCE_FIELD_LEN],
907 uint64_t session_id, const uint8_t *msg, size_t msg_len, uint8_t *out, size_t out_cap)
908{
909 const size_t key_len = pc_smb2_cipher_key_len(cipher);
910 const size_t nonce_len = pc_smb2_cipher_nonce_len(cipher);
911 if (!key || !nonce || !msg || !out || key_len == 0 || nonce_len == 0 ||
912 out_cap < PC_SMB2_TRANSFORM_HDR_LEN + msg_len)
913 {
914 return 0;
915 }
916
917 // TRANSFORM_HEADER (MS-SMB2 §2.2.41). Signature (the AEAD tag) is filled after sealing; it and the
918 // ProtocolId are outside the AAD, which is the header from the Nonce field to its end (offsets 20..51).
919 memset(out, 0, PC_SMB2_TRANSFORM_HDR_LEN);
920 pc_wr32le(out + 0, PC_SMB2_TRANSFORM_PROTOCOL_ID); // ProtocolId 0xFD 'S' 'M' 'B'
921 memcpy(out + 20, nonce, PC_SMB2_NONCE_FIELD_LEN); // Nonce field (16 bytes; AEAD uses the leading bytes)
922 pc_wr32le(out + 36, (uint32_t)msg_len); // OriginalMessageSize
923 pc_wr16le(out + 40, 0); // Reserved
924 pc_wr16le(out + 42, 0x0001); // Flags = Encrypted (3.1.1)
925 pc_wr64le(out + 44, session_id); // SessionId
926
927 uint8_t *ct = out + PC_SMB2_TRANSFORM_HDR_LEN;
928 const uint8_t *aad = out + 20; // 32 bytes: Nonce field .. SessionId
929 uint8_t tag[16];
930 bool ok = false;
931 switch (cipher)
932 {
933 case Smb2Cipher::SMB2_ENCRYPTION_AES128_GCM: {
934 // Per-call context: same reasoning as the AES-256 branch below - not a hot enough path to justify
935 // a per-session one, and the lifecycle cost is at least visible here.
937 pc_aes128gcm_key *g = pc_aes128gcm_key_init(g_b.span().buf, key);
938 pc_aes128gcm_seal(g, out + 20, aad, 32, msg, msg_len, ct, tag);
939 pc_aes128gcm_key_wipe(g);
940 }
941 ok = true;
942 break;
943 case Smb2Cipher::SMB2_ENCRYPTION_AES256_GCM: {
944 // Per-call context: this path is not hot enough to justify a per-session one. The cost is the
945 // ~9,200-cycle lifecycle documented in aesgcm.h - hoist the context into session state if it
946 // ever shows up in a profile.
948 pc_aesgcm_key *gcm = pc_aesgcm_key_init(gcm_b.span().buf, key);
949 pc_aesgcm_seal(gcm, out + 20, aad, 32, msg, msg_len, ct, tag);
951 }
952 ok = true;
953 break;
954 case Smb2Cipher::SMB2_ENCRYPTION_AES128_CCM:
955 case Smb2Cipher::SMB2_ENCRYPTION_AES256_CCM:
956 ok = pc_aesccm_seal_tag(key, key_len, out + 20, nonce_len, aad, 32, msg, msg_len, ct, tag);
957 break;
958 default:
959 return 0;
960 }
961 if (!ok)
962 {
963 return 0;
964 }
965 memcpy(out + 4, tag, 16); // Signature = the 16-byte AEAD tag
966 return PC_SMB2_TRANSFORM_HDR_LEN + msg_len;
967}
968
969size_t pc_smb2_decrypt(uint16_t cipher, const uint8_t *key, const uint8_t *in, size_t in_len, uint8_t *out,
970 size_t out_cap)
971{
972 const size_t key_len = pc_smb2_cipher_key_len(cipher);
973 const size_t nonce_len = pc_smb2_cipher_nonce_len(cipher);
974 if (!key || !in || !out || key_len == 0 || nonce_len == 0 || in_len < PC_SMB2_TRANSFORM_HDR_LEN)
975 {
976 return 0;
977 }
978 if (pc_rd32le(in + 0) != PC_SMB2_TRANSFORM_PROTOCOL_ID)
979 {
980 return 0;
981 }
982 size_t ct_len = in_len - PC_SMB2_TRANSFORM_HDR_LEN;
983 if (pc_rd32le(in + 36) != (uint32_t)ct_len || out_cap < ct_len) // OriginalMessageSize must match
984 {
985 return 0;
986 }
987
988 // AAD = header[20..51] (32 bytes), tag = Signature (in+4), ciphertext = in+52; the AEAD nonce is the leading
989 // bytes of the AAD (the Nonce field). Snapshot the AAD and tag before decrypting: callers decrypt in place
990 // (out aliases in), and the CCM open writes the recovered plaintext over the header before it reads the AAD
991 // and tag to compute/verify the MAC - so reading them from @p in after decryption would see clobbered bytes.
992 // Copying the 48 header bytes makes in-place decryption safe for every cipher. Fails closed on a bad tag.
993 uint8_t aad[32];
994 uint8_t tag[16];
995 memcpy(aad, in + 20, 32);
996 memcpy(tag, in + 4, 16);
997 const uint8_t *ct = in + PC_SMB2_TRANSFORM_HDR_LEN;
998 bool ok = false;
999 switch (cipher)
1000 {
1001 case Smb2Cipher::SMB2_ENCRYPTION_AES128_GCM: {
1002 // Per-call context: same reasoning as the AES-256 branch below - not a hot enough path to justify
1003 // a per-session one, and the lifecycle cost is at least visible here.
1005 pc_aes128gcm_key *g = pc_aes128gcm_key_init(g_b.span().buf, key);
1006 ok = pc_aes128gcm_open(g, aad, aad, 32, ct, ct_len, tag, out);
1007 pc_aes128gcm_key_wipe(g);
1008 }
1009 break;
1010 case Smb2Cipher::SMB2_ENCRYPTION_AES256_GCM: {
1011 // Per-call context: this path is not hot enough to justify a per-session one. The cost is the
1012 // ~9,200-cycle lifecycle documented in aesgcm.h - hoist the context into session state if it
1013 // ever shows up in a profile.
1014 SecureBorrow gcm_b(PC_WORK_AESGCM, 8);
1015 pc_aesgcm_key *gcm = pc_aesgcm_key_init(gcm_b.span().buf, key);
1016 ok = pc_aesgcm_open(gcm, aad, aad, 32, ct, ct_len, tag, out);
1017 pc_aesgcm_key_wipe(gcm);
1018 }
1019 break;
1020 case Smb2Cipher::SMB2_ENCRYPTION_AES128_CCM:
1021 case Smb2Cipher::SMB2_ENCRYPTION_AES256_CCM:
1022 ok = pc_aesccm_open_tag(key, key_len, aad, nonce_len, aad, 32, ct, ct_len, tag, out);
1023 break;
1024 default:
1025 return 0;
1026 }
1027 return ok ? ct_len : 0;
1028}
1029
1030#endif // PC_ENABLE_SMB
AES-128 block cipher + AEAD_AES_128_GCM (RFC 5116 / NIST SP 800-38D).
void pc_aes_cmac(const uint8_t key[16], const uint8_t *msg, size_t msg_len, uint8_t mac[PC_AES_CMAC_LEN])
Compute the AES-128-CMAC of msg under key (RFC 4493).
Definition aes_cmac.cpp:110
AES-128-CMAC (RFC 4493 / NIST SP800-38B) one-shot MAC.
AEAD AES-CCM (NIST SP 800-38C / RFC 3610), 128- and 256-bit keys, detached tag.
AES-256-GCM AEAD (RFC 5116) - stateless, detached-tag API.
A secure borrow whose acquire and release are one call each.
Definition secure.h:153
Fixed-width integer serializers into a raw uint8_t* buffer - one source of truth.
size_t pc_wr64le(uint8_t *p, uint64_t v)
Write v little-endian at p.
Definition endian.h:50
uint64_t pc_rd64le(const uint8_t *p)
Read a little-endian u64 at p.
Definition endian.h:72
uint32_t pc_rd32le(const uint8_t *p)
Read a little-endian u32 at p.
Definition endian.h:66
size_t pc_wr32le(uint8_t *p, uint32_t v)
Write v little-endian at p.
Definition endian.h:40
uint16_t pc_rd16le(const uint8_t *p)
Read a little-endian u16 at p.
Definition endian.h:60
size_t pc_wr16le(uint8_t *p, uint16_t v)
Write v little-endian at p.
Definition endian.h:32
void pc_hmac_sha256(const uint8_t *key, size_t key_len, const uint8_t *data, size_t len, uint8_t mac[PC_HMAC_SHA256_LEN])
Compute HMAC-SHA2-256 over a single contiguous buffer.
HMAC-SHA2-256 (RFC 2104 + FIPS 198-1) - streaming context and one-shot API.
PC_CRYPTO_HOT bool pc_kdf_ctr_hmac_sha256(const uint8_t *ki, size_t ki_len, const uint8_t *fixed, size_t fixed_len, uint8_t *out, size_t out_len)
SP800-108 KDF in counter mode with HMAC-SHA256 as the PRF (NIST SP800-108 §5.1; r = 32-bit counter pl...
Definition kdf.cpp:20
SP800-108 counter-mode key derivation (HMAC-SHA256 PRF).
bool pc_aesgcm_open(pc_aesgcm_key *k, const uint8_t nonce[PC_AESGCM_IV_LEN], const uint8_t *aad, size_t aad_len, const uint8_t *ct, size_t ct_len, const uint8_t tag[PC_AESGCM_TAG_LEN], uint8_t *out)
Open one record: verify tag over aad || ct in constant time, then (only on success) decrypt ct into o...
pc_aesgcm_key * pc_aesgcm_key_init(void *storage, const uint8_t key[PC_AESGCM_KEY_LEN])
Bind storage as a context keyed with key.
pc_cspan pc_aesgcm_seal(pc_aesgcm_key *k, const uint8_t nonce[PC_AESGCM_IV_LEN], const uint8_t *aad, size_t aad_len, const uint8_t *pt, size_t pt_len, uint8_t *ct_out, uint8_t tag_out[PC_AESGCM_TAG_LEN])
Seal one record under k and nonce.
void pc_aesgcm_key_wipe(pc_aesgcm_key *k)
Wipe the expanded schedule. Call on rekey and on close; the storage stays the caller's.
#define PC_WORK_AES128GCM
#define PC_WORK_AESGCM
Secure pool accessor - borrows that hold key material.
void pc_sha512_update(pc_sha512_ctx *ctx, const uint8_t *data, size_t len)
Feed len bytes of data into the running hash.
Definition sha512.cpp:38
PC_CRYPTO_HOT void pc_sha512_init(pc_sha512_ctx *ctx)
Initialize a streaming SHA-512 context (ctx must not be NULL).
Definition sha512.cpp:28
void pc_sha512_final(pc_sha512_ctx *ctx, uint8_t digest[PC_SHA512_DIGEST_LEN])
Finalize the hash and write the 64-byte digest. The context is undefined afterwards; call init() agai...
Definition sha512.cpp:47
SHA-512 (FIPS 180-4) - streaming context and one-shot API.
SMB2 client wire codec (MS-SMB2), PC_ENABLE_SMB - increment 1: the transport frame,...
Streaming SHA-512 context.
Definition sha512.h:33