blob: aa3d63ca3434ba80526c26f8438a9ceeb23db212 [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3namespace Adldap\Models\Attributes;
4
5class TSProperty
6{
7 /**
8 * Nibble control values. The first value for each is if the nibble is <= 9, otherwise the second value is used.
9 */
10 const NIBBLE_CONTROL = [
11 'X' => ['001011', '011010'],
12 'Y' => ['001110', '011010'],
13 ];
14
15 /**
16 * The nibble header.
17 */
18 const NIBBLE_HEADER = '1110';
19
20 /**
21 * Conversion factor needed for time values in the TSPropertyArray (stored in microseconds).
22 */
23 const TIME_CONVERSION = 60 * 1000;
24
25 /**
26 * A simple map to help determine how the property needs to be decoded/encoded from/to its binary value.
27 *
28 * There are some names that are simple repeats but have 'W' at the end. Not sure as to what that signifies. I
29 * cannot find any information on them in Microsoft documentation. However, their values appear to stay in sync with
30 * their non 'W' counterparts. But not doing so when manipulating the data manually does not seem to affect anything.
31 * This probably needs more investigation.
32 *
33 * @var array
34 */
35 protected $propTypes = [
36 'string' => [
37 'CtxWFHomeDir',
38 'CtxWFHomeDirW',
39 'CtxWFHomeDirDrive',
40 'CtxWFHomeDirDriveW',
41 'CtxInitialProgram',
42 'CtxInitialProgramW',
43 'CtxWFProfilePath',
44 'CtxWFProfilePathW',
45 'CtxWorkDirectory',
46 'CtxWorkDirectoryW',
47 'CtxCallbackNumber',
48 ],
49 'time' => [
50 'CtxMaxDisconnectionTime',
51 'CtxMaxConnectionTime',
52 'CtxMaxIdleTime',
53 ],
54 'int' => [
55 'CtxCfgFlags1',
56 'CtxCfgPresent',
57 'CtxKeyboardLayout',
58 'CtxMinEncryptionLevel',
59 'CtxNWLogonServer',
60 'CtxShadow',
61 ],
62 ];
63
64 /**
65 * The property name.
66 *
67 * @var string
68 */
69 protected $name;
70
71 /**
72 * The property value.
73 *
74 * @var string|int
75 */
76 protected $value;
77
78 /**
79 * The property value type.
80 *
81 * @var int
82 */
83 protected $valueType = 1;
84
85 /**
86 * Pass binary TSProperty data to construct its object representation.
87 *
88 * @param string|null $value
89 */
90 public function __construct($value = null)
91 {
92 if ($value) {
93 $this->decode(bin2hex($value));
94 }
95 }
96
97 /**
98 * Set the name for the TSProperty.
99 *
100 * @param string $name
101 *
102 * @return TSProperty
103 */
104 public function setName($name)
105 {
106 $this->name = $name;
107
108 return $this;
109 }
110
111 /**
112 * Get the name for the TSProperty.
113 *
114 * @return string
115 */
116 public function getName()
117 {
118 return $this->name;
119 }
120
121 /**
122 * Set the value for the TSProperty.
123 *
124 * @param string|int $value
125 *
126 * @return TSProperty
127 */
128 public function setValue($value)
129 {
130 $this->value = $value;
131
132 return $this;
133 }
134
135 /**
136 * Get the value for the TSProperty.
137 *
138 * @return string|int
139 */
140 public function getValue()
141 {
142 return $this->value;
143 }
144
145 /**
146 * Convert the TSProperty name/value back to its binary
147 * representation for the userParameters blob.
148 *
149 * @return string
150 */
151 public function toBinary()
152 {
153 $name = bin2hex($this->name);
154
155 $binValue = $this->getEncodedValueForProp($this->name, $this->value);
156
157 $valueLen = strlen(bin2hex($binValue)) / 3;
158
159 $binary = hex2bin(
160 $this->dec2hex(strlen($name))
161 .$this->dec2hex($valueLen)
162 .$this->dec2hex($this->valueType)
163 .$name
164 );
165
166 return $binary.$binValue;
167 }
168
169 /**
170 * Given a TSProperty blob, decode the name/value/type/etc.
171 *
172 * @param string $tsProperty
173 */
174 protected function decode($tsProperty)
175 {
176 $nameLength = hexdec(substr($tsProperty, 0, 2));
177
178 // 1 data byte is 3 encoded bytes
179 $valueLength = hexdec(substr($tsProperty, 2, 2)) * 3;
180
181 $this->valueType = hexdec(substr($tsProperty, 4, 2));
182 $this->name = pack('H*', substr($tsProperty, 6, $nameLength));
183 $this->value = $this->getDecodedValueForProp($this->name, substr($tsProperty, 6 + $nameLength, $valueLength));
184 }
185
186 /**
187 * Based on the property name/value in question, get its encoded form.
188 *
189 * @param string $propName
190 * @param string|int $propValue
191 *
192 * @return string
193 */
194 protected function getEncodedValueForProp($propName, $propValue)
195 {
196 if (in_array($propName, $this->propTypes['string'])) {
197 // Simple strings are null terminated. Unsure if this is
198 // needed or simply a product of how ADUC does stuff?
199 $value = $this->encodePropValue($propValue."\0", true);
200 } elseif (in_array($propName, $this->propTypes['time'])) {
201 // Needs to be in microseconds (assuming it is in minute format)...
202 $value = $this->encodePropValue($propValue * self::TIME_CONVERSION);
203 } else {
204 $value = $this->encodePropValue($propValue);
205 }
206
207 return $value;
208 }
209
210 /**
211 * Based on the property name in question, get its actual value from the binary blob value.
212 *
213 * @param string $propName
214 * @param string $propValue
215 *
216 * @return string|int
217 */
218 protected function getDecodedValueForProp($propName, $propValue)
219 {
220 if (in_array($propName, $this->propTypes['string'])) {
221 // Strip away null terminators. I think this should
222 // be desired, otherwise it just ends in confusion.
223 $value = str_replace("\0", '', $this->decodePropValue($propValue, true));
224 } elseif (in_array($propName, $this->propTypes['time'])) {
225 // Convert from microseconds to minutes (how ADUC displays
226 // it anyway, and seems the most practical).
227 $value = hexdec($this->decodePropValue($propValue)) / self::TIME_CONVERSION;
228 } elseif (in_array($propName, $this->propTypes['int'])) {
229 $value = hexdec($this->decodePropValue($propValue));
230 } else {
231 $value = $this->decodePropValue($propValue);
232 }
233
234 return $value;
235 }
236
237 /**
238 * Decode the property by inspecting the nibbles of each blob, checking
239 * the control, and adding up the results into a final value.
240 *
241 * @param string $hex
242 * @param bool $string Whether or not this is simple string data.
243 *
244 * @return string
245 */
246 protected function decodePropValue($hex, $string = false)
247 {
248 $decodePropValue = '';
249
250 $blobs = str_split($hex, 6);
251
252 foreach ($blobs as $blob) {
253 $bin = decbin(hexdec($blob));
254
255 $controlY = substr($bin, 4, 6);
256 $nibbleY = substr($bin, 10, 4);
257 $controlX = substr($bin, 14, 6);
258 $nibbleX = substr($bin, 20, 4);
259
260 $byte = $this->nibbleControl($nibbleX, $controlX).$this->nibbleControl($nibbleY, $controlY);
261
262 if ($string) {
263 $decodePropValue .= MbString::chr(bindec($byte));
264 } else {
265 $decodePropValue = $this->dec2hex(bindec($byte)).$decodePropValue;
266 }
267 }
268
269 return $decodePropValue;
270 }
271
272 /**
273 * Get the encoded property value as a binary blob.
274 *
275 * @param string $value
276 * @param bool $string
277 *
278 * @return string
279 */
280 protected function encodePropValue($value, $string = false)
281 {
282 // An int must be properly padded. (then split and reversed).
283 // For a string, we just split the chars. This seems
284 // to be the easiest way to handle UTF-8 characters
285 // instead of trying to work with their hex values.
286 $chars = $string ? MbString::split($value) : array_reverse(str_split($this->dec2hex($value, 8), 2));
287
288 $encoded = '';
289
290 foreach ($chars as $char) {
291 // Get the bits for the char. Using this method to ensure it is fully padded.
292 $bits = sprintf('%08b', $string ? MbString::ord($char) : hexdec($char));
293 $nibbleX = substr($bits, 0, 4);
294 $nibbleY = substr($bits, 4, 4);
295
296 // Construct the value with the header, high nibble, then low nibble.
297 $value = self::NIBBLE_HEADER;
298
299 foreach (['Y' => $nibbleY, 'X' => $nibbleX] as $nibbleType => $nibble) {
300 $value .= $this->getNibbleWithControl($nibbleType, $nibble);
301 }
302
303 // Convert it back to a binary bit stream
304 foreach ([0, 8, 16] as $start) {
305 $encoded .= $this->packBitString(substr($value, $start, 8), 8);
306 }
307 }
308
309 return $encoded;
310 }
311
312 /**
313 * PHP's pack() function has no 'b' or 'B' template. This is
314 * a workaround that turns a literal bit-string into a
315 * packed byte-string with 8 bits per byte.
316 *
317 * @param string $bits
318 * @param bool $len
319 *
320 * @return string
321 */
322 protected function packBitString($bits, $len)
323 {
324 $bits = substr($bits, 0, $len);
325 // Pad input with zeros to next multiple of 4 above $len
326 $bits = str_pad($bits, 4 * (int) (($len + 3) / 4), '0');
327
328 // Split input into chunks of 4 bits, convert each to hex and pack them
329 $nibbles = str_split($bits, 4);
330 foreach ($nibbles as $i => $nibble) {
331 $nibbles[$i] = base_convert($nibble, 2, 16);
332 }
333
334 return pack('H*', implode('', $nibbles));
335 }
336
337 /**
338 * Based on the control, adjust the nibble accordingly.
339 *
340 * @param string $nibble
341 * @param string $control
342 *
343 * @return string
344 */
345 protected function nibbleControl($nibble, $control)
346 {
347 // This control stays constant for the low/high nibbles,
348 // so it doesn't matter which we compare to
349 if ($control == self::NIBBLE_CONTROL['X'][1]) {
350 $dec = bindec($nibble);
351 $dec += 9;
352 $nibble = str_pad(decbin($dec), 4, '0', STR_PAD_LEFT);
353 }
354
355 return $nibble;
356 }
357
358 /**
359 * Get the nibble value with the control prefixed.
360 *
361 * If the nibble dec is <= 9, the control X equals 001011 and Y equals 001110, otherwise if the nibble dec is > 9
362 * the control for X or Y equals 011010. Additionally, if the dec value of the nibble is > 9, then the nibble value
363 * must be subtracted by 9 before the final value is constructed.
364 *
365 * @param string $nibbleType Either X or Y
366 * @param string $nibble
367 *
368 * @return string
369 */
370 protected function getNibbleWithControl($nibbleType, $nibble)
371 {
372 $dec = bindec($nibble);
373
374 if ($dec > 9) {
375 $dec -= 9;
376 $control = self::NIBBLE_CONTROL[$nibbleType][1];
377 } else {
378 $control = self::NIBBLE_CONTROL[$nibbleType][0];
379 }
380
381 return $control.sprintf('%04d', decbin($dec));
382 }
383
384 /**
385 * Need to make sure hex values are always an even length, so pad as needed.
386 *
387 * @param int $int
388 * @param int $padLength The hex string must be padded to this length (with zeros).
389 *
390 * @return string
391 */
392 protected function dec2hex($int, $padLength = 2)
393 {
394 return str_pad(dechex($int), $padLength, 0, STR_PAD_LEFT);
395 }
396}