ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ntlm.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 ntlm.cpp
6 * @brief NTLMv2 response computation (see ntlm.h).
7 */
8
9#include "ntlm.h"
10
11#if PC_ENABLE_SMB
12
13#include "crypto/hash/md.h"
14#include "server/mmgr/secure.h" // SecureScope: lifetime of the borrowed digest state
15#include <string.h>
16
17void pc_ntlm_nt_hash(const char *password, uint8_t nt_hash[16])
18{
19 SecureScope scope; // the borrow below is reclaimed - and wiped - when this returns
20 MdCtx *c = pc_md_wants();
21 if (c == nullptr)
22 {
23 return;
24 }
25 pc_md4_init(c);
26 for (const char *p = password; *p; p++)
27 {
28 uint8_t pair[2] = {(uint8_t)*p, 0}; // UTF-16LE (ASCII/UTF-8 code unit + high byte 0)
29 pc_md4_update(c, pair, 2);
30 }
31 pc_md4_final(c, nt_hash);
32}
33
34bool pc_ntlm_ntowfv2(const uint8_t nt_hash[16], const char *user, const char *domain, uint8_t owf[16])
35{
36 uint8_t buf[512]; // UTF-16LE of Uppercase(user) + domain; 256 chars max
37 size_t n = 0;
38 for (const char *p = user; *p; p++)
39 {
40 char up = *p;
41 if (up >= 'a' && up <= 'z')
42 {
43 up = (char)(up - 32); // ASCII uppercase (only the user, per MS-NLMP)
44 }
45 if (n + 2 > sizeof(buf))
46 {
47 return false;
48 }
49 buf[n++] = (uint8_t)up;
50 buf[n++] = 0;
51 }
52 for (const char *p = domain; *p; p++)
53 {
54 if (n + 2 > sizeof(buf))
55 {
56 return false;
57 }
58 buf[n++] = (uint8_t)*p;
59 buf[n++] = 0;
60 }
61 pc_hmac_md5(nt_hash, 16, buf, n, owf);
62 return true;
63}
64
65// HMAC-MD5 over a two-part message (the key here is always the 16-byte NTOWFv2, < 64 bytes,
66// so no key-shortening is needed).
67static void pc_hmac_md5_2(const uint8_t key[16], const uint8_t *m1, size_t l1, const uint8_t *m2, size_t l2,
68 uint8_t out[16])
69{
70 uint8_t ipad[64];
71 uint8_t opad[64];
72 for (int i = 0; i < 64; i++)
73 {
74 uint8_t k = (i < 16) ? key[i] : 0;
75 ipad[i] = (uint8_t)(k ^ 0x36);
76 opad[i] = (uint8_t)(k ^ 0x5c);
77 }
78 uint8_t inner[16];
79 SecureScope scope; // the borrow below is reclaimed - and wiped - when this returns
80 MdCtx *c = pc_md_wants();
81 if (c == nullptr)
82 {
83 return;
84 }
85 pc_md5_init(c);
86 pc_md5_update(c, ipad, 64);
87 pc_md5_update(c, m1, l1);
88 if (m2 && l2) // GCOVR_EXCL_LINE the sole caller always passes temp: non-null and always >= 32 bytes
89 {
90 pc_md5_update(c, m2, l2);
91 }
92 pc_md5_final(c, inner);
93 pc_md5_init(c);
94 pc_md5_update(c, opad, 64);
95 pc_md5_update(c, inner, 16);
96 pc_md5_final(c, out);
97}
98
99size_t pc_ntlm_v2_response(const uint8_t owf[16], const uint8_t server_challenge[8], const uint8_t client_challenge[8],
100 const uint8_t timestamp[8], const uint8_t *target_info, size_t ti_len, uint8_t *out,
101 size_t out_cap, uint8_t session_key[16])
102{
103 const size_t temp_len = 2 + 6 + 8 + 8 + 4 + ti_len + 4; // MS-NLMP temp layout
104 const size_t pc_resp_len = 16 + temp_len; // NTProofStr(16) + temp
105 if (!out || pc_resp_len > out_cap)
106 {
107 return 0;
108 }
109
110 // Build temp in place at out+16, so the result is NTProofStr(16) || temp contiguously.
111 uint8_t *temp = out + 16;
112 size_t k = 0;
113 temp[k++] = 0x01; // Responserversion
114 temp[k++] = 0x01; // HiResponserversion
115 memset(temp + k, 0, 6); // Z(6)
116 k += 6;
117 memcpy(temp + k, timestamp, 8);
118 k += 8;
119 memcpy(temp + k, client_challenge, 8);
120 k += 8;
121 memset(temp + k, 0, 4); // Z(4)
122 k += 4;
123 memcpy(temp + k, target_info, ti_len);
124 k += ti_len;
125 memset(temp + k, 0, 4); // Z(4) trailer; temp_len (line 83) already accounts for it, so k is done
126
127 uint8_t ntproof[16];
128 pc_hmac_md5_2(owf, server_challenge, 8, temp, temp_len, ntproof);
129 memcpy(out, ntproof, 16); // out = NTProofStr || temp
130 if (session_key)
131 {
132 pc_hmac_md5(owf, 16, ntproof, 16, session_key);
133 }
134 return pc_resp_len;
135}
136
137size_t pc_ntlm_set_mic_flag(const uint8_t *target_info, size_t ti_len, uint8_t *out, size_t out_cap)
138{
139 if (!target_info || !out)
140 {
141 return 0;
142 }
143 // Walk the AV_PAIR list (AvId u16, AvLen u16, Value[AvLen]), copying it and, if an MsvAvFlags pair
144 // (AvId 6) is present, OR-ing bit 0x2 into its 32-bit LE value in place. Track the EOL position so a
145 // missing pair can be inserted there.
146 if (ti_len > out_cap)
147 {
148 return 0;
149 }
150 memcpy(out, target_info, ti_len);
151 size_t p = 0;
152 bool found = false;
153 size_t eol = ti_len; // offset of the MsvAvEOL header, if any
154 while (p + 4 <= ti_len)
155 {
156 uint16_t id = (uint16_t)(out[p] | (out[p + 1] << 8));
157 uint16_t len = (uint16_t)(out[p + 2] | (out[p + 3] << 8));
158 if (id == 0) // MsvAvEOL
159 {
160 eol = p;
161 break;
162 }
163 if (id == 6 && len == 4 && p + 8 <= ti_len)
164 {
165 out[p + 4] |= 0x02; // MsvAvFlags: set "MIC provided" in the low byte of the LE value
166 found = true;
167 }
168 if (p + 4 + len < p + 4) // overflow guard
169 {
170 return 0;
171 }
172 p += 4 + len;
173 }
174 if (found)
175 {
176 return ti_len;
177 }
178 // Insert a fresh MsvAvFlags pair (AvId 6, AvLen 4, value 0x00000002). A well-formed list has an EOL
179 // (AvId 0) terminator - splice the pair in just before it; a fixture without one (or that ran off the
180 // end) gets the pair appended at the tail, matching the pre-MIC pass-through leniency (never fail here).
181 if (ti_len + 8 > out_cap)
182 {
183 return 0;
184 }
185 const size_t at = eol != ti_len ? eol : ti_len;
186 if (at < ti_len)
187 {
188 memmove(out + at + 8, out + at, ti_len - at); // shift the EOL (and anything after) up by 8
189 }
190 out[at + 0] = 0x06;
191 out[at + 1] = 0x00;
192 out[at + 2] = 0x04;
193 out[at + 3] = 0x00;
194 out[at + 4] = 0x02;
195 out[at + 5] = 0x00;
196 out[at + 6] = 0x00;
197 out[at + 7] = 0x00;
198 return ti_len + 8;
199}
200
201void pc_ntlm_mic(const uint8_t session_key[16], const uint8_t *neg, size_t neg_len, const uint8_t *chal,
202 size_t chal_len, const uint8_t *auth, size_t auth_len, uint8_t out[16])
203{
204 // HMAC-MD5(session_key, neg || chal || auth), streamed. The key is 16 bytes (< 64), no shortening.
205 uint8_t ipad[64];
206 uint8_t opad[64];
207 for (int i = 0; i < 64; i++)
208 {
209 uint8_t k = i < 16 ? session_key[i] : 0;
210 ipad[i] = (uint8_t)(k ^ 0x36);
211 opad[i] = (uint8_t)(k ^ 0x5c);
212 }
213 SecureScope scope; // the borrow below is reclaimed - and wiped - when this returns
214 MdCtx *c = pc_md_wants();
215 if (c == nullptr)
216 {
217 return;
218 }
219 uint8_t inner[16];
220 pc_md5_init(c);
221 pc_md5_update(c, ipad, 64);
222 pc_md5_update(c, neg, neg_len);
223 pc_md5_update(c, chal, chal_len);
224 pc_md5_update(c, auth, auth_len);
225 pc_md5_final(c, inner);
226 pc_md5_init(c);
227 pc_md5_update(c, opad, 64);
228 pc_md5_update(c, inner, 16);
229 pc_md5_final(c, out);
230}
231
232#endif // PC_ENABLE_SMB
RAII scope guard for secure borrows - marks on entry, wipes and reclaims on every exit.
Definition secure.h:179
void pc_md5_init(MdCtx *c)
Definition md.cpp:103
void pc_md4_init(MdCtx *c)
Definition md.cpp:192
void pc_md4_update(MdCtx *c, const uint8_t *data, size_t len)
Definition md.cpp:258
MdCtx * pc_md_wants(void)
Storage this module wants for one MD4/MD5 context.
Definition md.cpp:28
void pc_md5_update(MdCtx *c, const uint8_t *data, size_t len)
Definition md.cpp:250
void pc_md5_final(MdCtx *c, uint8_t out[16])
Definition md.cpp:254
void pc_md4_final(MdCtx *c, uint8_t out[16])
Definition md.cpp:262
void pc_hmac_md5(const uint8_t *key, size_t key_len, const uint8_t *msg, size_t msg_len, uint8_t out[16])
HMAC-MD5 (RFC 2104): the NTLMv2 MAC primitive.
Definition md.cpp:284
MD4 (RFC 1320), MD5 (RFC 1321), and HMAC-MD5 (RFC 2104) - the legacy digests NTLM needs.
NTLMv2 response computation (MS-NLMP ยง3.3.2) for the SMB2 client (PC_ENABLE_SMB).
Secure pool accessor - borrows that hold key material.
Definition md.cpp:19