Browse code

Bump copyright.

Xavier G authored on04/04/2022 19:21:51
Showing1 changed files
... ...
@@ -1,6 +1,6 @@
1 1
 #!/usr/bin/env python3
2 2
 
3
-# Copyright © 2020 Xavier G. <xavier.yamltab@kindwolf.org>
3
+# Copyright © 2020-2022 Xavier G. <xavier.yamltab@kindwolf.org>
4 4
 # This work is free. You can redistribute it and/or modify it under the
5 5
 # terms of the Do What The Fuck You Want To Public License, Version 2,
6 6
 # as published by Sam Hocevar. See the COPYING file for more details.
Browse code

Fix spn_to_principal().

Xavier G authored on04/04/2022 19:15:48
Showing1 changed files
... ...
@@ -430,7 +430,7 @@ def spn_to_principal(spn, name_type='KRB5_NT_PRINCIPAL'):
430 430
 		raise KeytabComposingError('Cannot parse SPN %s into principal' % spn)
431 431
 	principal['realm'] = rem.group('realm')
432 432
 	principal['components'] = []
433
-	for name, component in rem.groupdict():
433
+	for name, component in rem.groupdict().items():
434 434
 		if name.startswith('comp') and component is not None:
435 435
 			principal['components'].append(component)
436 436
 	return principal
Browse code

YAML output: handle lack of support for sort_keys.

Xavier G authored on11/05/2020 20:58:14
Showing1 changed files
... ...
@@ -315,7 +315,15 @@ def keytab_to_yaml(buf, args):
315 315
 def output_data(data, args, exit=None):
316 316
 	serialized_data = prepare_serialization(data)
317 317
 	if args.output_format == 'yaml':
318
-		yaml.dump(serialized_data, sys.stdout, width=160, sort_keys=False)
318
+		yaml_width = 160
319
+		try:
320
+			yaml.dump(serialized_data, sys.stdout, width=yaml_width, sort_keys=False)
321
+		except TypeError as error:
322
+			# Handle lack of support for sort_keys:
323
+			if 'sort_keys' in str(error):
324
+				yaml.dump(serialized_data, sys.stdout, width=yaml_width)
325
+			else:
326
+				raise
319 327
 	else:
320 328
 		json.dump(serialized_data, sys.stdout, indent=4)
321 329
 	if exit is not None:
Browse code

Support timezones in date format.

Also use non-naive datetime objects.

Xavier G authored on11/05/2020 20:42:31
Showing1 changed files
... ...
@@ -26,7 +26,7 @@ DATA_LAYOUT_FULL = 1
26 26
 DATA_LAYOUT_SIMPLE = 2
27 27
 DATA_LAYOUTS = {'raw': DATA_LAYOUT_RAW, 'full': DATA_LAYOUT_FULL, 'simple': DATA_LAYOUT_SIMPLE}
28 28
 
29
-DATE_TIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
29
+DATE_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S%z'
30 30
 
31 31
 KEYTAB_FIRST_BYTE = 0x05
32 32
 # Default prefix for struct's format strings, defining big-endian byte order:
... ...
@@ -231,7 +231,7 @@ def enrich_keytab(keytab):
231 231
 		if 'entry' not in record:
232 232
 			continue
233 233
 		entry = record['entry']
234
-		entry['date'] = datetime.utcfromtimestamp(entry['timestamp']).strftime(DATE_TIME_FORMAT)
234
+		entry['date'] = datetime.fromtimestamp(entry['timestamp'], tz=timezone.utc).strftime(DATE_TIME_FORMAT)
235 235
 		if 'name_type_raw' in entry['principal']:
236 236
 			entry['principal']['name_type'] = int_to_name_type(entry['principal']['name_type_raw'])
237 237
 		spn = principal_to_spn(entry['principal'])
Browse code

Fix handling of "now" as a date value.

Xavier G authored on11/05/2020 17:29:07
Showing1 changed files
... ...
@@ -13,7 +13,7 @@ import json
13 13
 import yaml
14 14
 import struct
15 15
 import argparse
16
-from datetime import datetime
16
+from datetime import datetime, timezone
17 17
 from binascii import hexlify, unhexlify
18 18
 
19 19
 # Documents used as reference to implement the keytab format:
... ...
@@ -37,6 +37,7 @@ DEFAULT_PREFIX=BIG_ENDIAN
37 37
 DEFAULT_ENCODING='ascii'
38 38
 VERBOSITY=1
39 39
 CALCSIZE={}
40
+now = None
40 41
 
41 42
 # The following table is based on [3]:
42 43
 NAME_TYPES = {
... ...
@@ -474,6 +475,9 @@ def simple_timestamp_to_full(inentry, index, entry):
474 475
 	elif 'date' in inentry:
475 476
 		entry['date'] = inentry['date']
476 477
 		if inentry['date'] == 'now':
478
+			global now
479
+			if now is None:
480
+				now = int(datetime.now(timezone.utc).timestamp())
477 481
 			entry['timestamp'] = now
478 482
 		else:
479 483
 			try:
... ...
@@ -497,7 +501,6 @@ def simple_enctype_to_full(inentry, index,  entry):
497 501
 	return 2
498 502
 
499 503
 def simple_keytab_to_full(indata):
500
-	now = int(datetime.now().timestamp())
501 504
 	data = {
502 505
 		'version': indata.get('version', 2),
503 506
 		'records': [],
Browse code

Add --name-types and --enc-types.

Xavier G authored on28/04/2020 20:27:11
Showing1 changed files
... ...
@@ -537,6 +537,8 @@ def parse_args():
537 537
 	parser.add_argument('--output-format', '-f', dest='output_format', choices=['json', 'yaml'], default='yaml', help='output format (keytab to YAML/JSON only)')
538 538
 	parser.add_argument('--v1-endianness', '-e', dest='v1_endianness', choices=['native', 'little', 'big'], default='native', help='Enforce endianness (keytab v1 to YAML/JSON only)')
539 539
 	parser.add_argument('--simple-to-full', dest='simple_to_full', action='store_true', default=False, help='Convert from simple to full layout (YAML/JSON input only)')
540
+	parser.add_argument('--name-types', dest='list_name_types', action='store_true', default=False, help='List all known name types.')
541
+	parser.add_argument('--enc-types', dest='list_enc_types', action='store_true', default=False, help='List all known encryption types.')
540 542
 	parser.add_argument('input', nargs='?', type=argparse.FileType('rb'), default=sys.stdin.buffer, help='input file; defaults to standard input')
541 543
 	args = parser.parse_args()
542 544
 	return args
... ...
@@ -545,6 +547,10 @@ def main():
545 547
 	args = parse_args()
546 548
 	global VERBOSITY
547 549
 	VERBOSITY=args.verbose
550
+	if args.list_name_types:
551
+		output_data(NAME_TYPES, args, 0)
552
+	if args.list_enc_types:
553
+		output_data(ENC_TYPES, args, 0)
548 554
 	buf = args.input
549 555
 	first_byte = buf.peek(1)[0]
550 556
 	if first_byte == KEYTAB_FIRST_BYTE:
Browse code

Improve simple-to-full layout conversion.

Avoid adding null "spn" properties.

Xavier G authored on26/04/2020 20:38:58
Showing1 changed files
... ...
@@ -446,7 +446,9 @@ def simple_principal_to_full(inentry, index, entry):
446 446
 				message = 'Invalid or unknown name_type specified in entry #%d'
447 447
 				message += '; use name_type_raw to enforce an arbitrary value'
448 448
 				raise KeytabParsingError(message % index)
449
-		entry['spn'] = principal_to_spn(principal)
449
+		spn = principal_to_spn(principal)
450
+		if spn:
451
+			entry['spn'] = spn
450 452
 	elif 'spn' in inentry:
451 453
 		entry['spn'] = inentry['spn']
452 454
 		entry['principal'] = spn_to_principal(inentry['spn'])
Browse code

Improve simple-to-full layout conversion.

yamltab now computes record lengths.

Xavier G authored on26/04/2020 20:38:01
Showing1 changed files
... ...
@@ -320,6 +320,12 @@ def output_data(data, args, exit=None):
320 320
 	if exit is not None:
321 321
 		sys.exit(exit)
322 322
 
323
+def len_data(data):
324
+	return 2 + len(data)
325
+
326
+def len_str(string, encoding=DEFAULT_ENCODING):
327
+	return len_data(string.encode(encoding))
328
+
323 329
 def pack_data(data):
324 330
 	return struct.pack('>H', len(data)) + data
325 331
 
... ...
@@ -420,6 +426,16 @@ def spn_to_principal(spn, name_type='KRB5_NT_PRINCIPAL'):
420 426
 			principal['components'].append(component)
421 427
 	return principal
422 428
 
429
+def principal_length(principal):
430
+	# component count:
431
+	length = 2
432
+	length += len_str(principal.get('realm', ''))
433
+	for component in principal.get('components', []):
434
+		length += len_str(component)
435
+	if principal.get('name_type_raw', principal.get('name_type')):
436
+		length += 4
437
+	return length
438
+
423 439
 def simple_principal_to_full(inentry, index, entry):
424 440
 	if 'principal' in inentry:
425 441
 		principal = entry['principal'] = inentry['principal']
... ...
@@ -434,6 +450,7 @@ def simple_principal_to_full(inentry, index, entry):
434 450
 	elif 'spn' in inentry:
435 451
 		entry['spn'] = inentry['spn']
436 452
 		entry['principal'] = spn_to_principal(inentry['spn'])
453
+	return principal_length(entry['principal'])
437 454
 
438 455
 def simple_kvno_to_full(inentry, index, entry, record):
439 456
 	if 'kvno' in inentry:
... ...
@@ -447,6 +464,7 @@ def simple_kvno_to_full(inentry, index, entry, record):
447 464
 			message = 'Cannot store kvno > 255 without kvno_in_tail in entry #%d'
448 465
 			raise KeytabComposingError(message % index)
449 466
 		record['tail'] += to_bytes(inentry.get('extra_tail', b''))
467
+	return 1
450 468
 
451 469
 def simple_timestamp_to_full(inentry, index, entry):
452 470
 	if 'timestamp' in inentry:
... ...
@@ -461,6 +479,7 @@ def simple_timestamp_to_full(inentry, index, entry):
461 479
 				entry['timestamp'] = int(parsed_date.timestamp())
462 480
 			except:
463 481
 				raise KeytabParsingError('Invalid date specified in entry #%d' % index)
482
+	return 4
464 483
 
465 484
 def simple_enctype_to_full(inentry, index,  entry):
466 485
 	if 'enctype_raw' in inentry:
... ...
@@ -473,6 +492,7 @@ def simple_enctype_to_full(inentry, index,  entry):
473 492
 			message = 'Invalid or unknown enctype specified in entry #%d'
474 493
 			message += '; use enctype_raw to enforce an arbitrary value'
475 494
 			raise KeytabParsingError(message % index)
495
+	return 2
476 496
 
477 497
 def simple_keytab_to_full(indata):
478 498
 	now = int(datetime.now().timestamp())
... ...
@@ -483,13 +503,17 @@ def simple_keytab_to_full(indata):
483 503
 	for index, inentry in enumerate(indata.get('entries', [])):
484 504
 		entry = {}
485 505
 		record = {'type': 'record', 'entry': entry, 'tail': b''}
486
-		simple_principal_to_full(inentry, index, entry)
487
-		simple_kvno_to_full(inentry, index, entry, record)
488
-		simple_timestamp_to_full(inentry, index, entry)
489
-		simple_enctype_to_full(inentry, index, entry)
506
+		length = 0
507
+		length += simple_principal_to_full(inentry, index, entry)
508
+		length += simple_kvno_to_full(inentry, index, entry, record)
509
+		length += simple_timestamp_to_full(inentry, index, entry)
510
+		length += simple_enctype_to_full(inentry, index, entry)
490 511
 		if 'key' in inentry:
491 512
 			entry['key'] = inentry['key']
492 513
 			entry['key_length'] = len(to_bytes(inentry['key']))
514
+			length += 2 + entry['key_length']
515
+		length += len(to_bytes(record['tail']))
516
+		record['length'] = length
493 517
 		data['records'].append(record)
494 518
 	return data
495 519
 
Browse code

Simple layout: introduce the "extra_tail" property.

Xavier G authored on26/04/2020 19:48:58
Showing1 changed files
... ...
@@ -268,6 +268,11 @@ def simplify_keytab(keytab):
268 268
 		simple_entry['kvno'] = entry.get('actual_kvno', entry['kvno'])
269 269
 		if 'tail_kvno' in entry:
270 270
 			simple_entry['kvno_in_tail'] = True
271
+		if 'tail' in record:
272
+			start = 4 if 'tail_kvno' in entry else 0
273
+			extra_tail = record['tail'][start:]
274
+			if extra_tail:
275
+				simple_entry['extra_tail'] = extra_tail
271 276
 		for key in ('date', 'enctype', 'key'):
272 277
 			if key in entry:
273 278
 				simple_entry[key] = entry[key]
... ...
@@ -441,6 +446,7 @@ def simple_kvno_to_full(inentry, index, entry, record):
441 446
 		elif kvno_too_big:
442 447
 			message = 'Cannot store kvno > 255 without kvno_in_tail in entry #%d'
443 448
 			raise KeytabComposingError(message % index)
449
+		record['tail'] += to_bytes(inentry.get('extra_tail', b''))
444 450
 
445 451
 def simple_timestamp_to_full(inentry, index, entry):
446 452
 	if 'timestamp' in inentry:
Browse code

Introduce to_bytes().

Xavier G authored on26/04/2020 19:41:55
Showing1 changed files
... ...
@@ -274,6 +274,11 @@ def simplify_keytab(keytab):
274 274
 		simplified['entries'].append(simple_entry)
275 275
 	return simplified
276 276
 
277
+def to_bytes(value):
278
+	if type(value) is bytes:
279
+		return value
280
+	return unhexlify(value)
281
+
277 282
 def prepare_serialization(obj):
278 283
 	"""
279 284
 	Prepare keytab for serialization.
... ...
@@ -343,7 +348,7 @@ def raw_entry_to_binary(entry, index):
343 348
 			raise KeytabComposingError('invalid %s data in entry #%d' % (key, index))
344 349
 	data = raw_principal_to_binary(entry['principal'], index)
345 350
 	try:
346
-		key_data = unhexlify(entry['key'])
351
+		key_data = to_bytes(entry['key'])
347 352
 	except:
348 353
 		raise KeytabComposingError('invalid key data in entry #%d' % index)
349 354
 	data += struct.pack('>IBHH', entry['timestamp'], entry['kvno'], entry['enctype_raw'], len(key_data))
... ...
@@ -357,7 +362,7 @@ def raw_record_to_binary(record, index):
357 362
 	data += raw_entry_to_binary(record['entry'], index)
358 363
 	if 'tail' in record:
359 364
 		try:
360
-			data += unhexlify(record['tail'])
365
+			data += to_bytes(record['tail'])
361 366
 		except:
362 367
 			raise KeytabComposingError('invalid tail data in record #%d' % index)
363 368
 	return struct.pack('>i', len(data)) + data
... ...
@@ -375,7 +380,7 @@ def raw_hole_to_binary(hole, index):
375 380
 		raise KeytabComposingError('invalid length in record #%d' % index)
376 381
 	if 'data' in record:
377 382
 		try:
378
-			hole_data = unhexlify(record['data'])
383
+			hole_data = to_bytes(record['data'])
379 384
 		except:
380 385
 			raise KeytabComposingError('invalid data in record #%d' % index)
381 386
 		if len(hole_data) != length:
... ...
@@ -432,7 +437,7 @@ def simple_kvno_to_full(inentry, index, entry, record):
432 437
 		entry['kvno'] = 0 if kvno_too_big else inentry['kvno']
433 438
 		if inentry.get('kvno_in_tail', False):
434 439
 			entry['tail_kvno'] = inentry['kvno']
435
-			record['tail'] = hexlify(struct.pack('>I', inentry['kvno']))
440
+			record['tail'] = struct.pack('>I', inentry['kvno'])
436 441
 		elif kvno_too_big:
437 442
 			message = 'Cannot store kvno > 255 without kvno_in_tail in entry #%d'
438 443
 			raise KeytabComposingError(message % index)
... ...
@@ -478,7 +483,7 @@ def simple_keytab_to_full(indata):
478 483
 		simple_enctype_to_full(inentry, index, entry)
479 484
 		if 'key' in inentry:
480 485
 			entry['key'] = inentry['key']
481
-			entry['key_length'] = len(unhexlify(inentry['key']))
486
+			entry['key_length'] = len(to_bytes(inentry['key']))
482 487
 		data['records'].append(record)
483 488
 	return data
484 489
 
Browse code

Improve simple-to-full layout conversion.

Xavier G authored on26/04/2020 19:14:49
Showing1 changed files
... ...
@@ -420,7 +420,9 @@ def simple_principal_to_full(inentry, index, entry):
420 420
 				message = 'Invalid or unknown name_type specified in entry #%d'
421 421
 				message += '; use name_type_raw to enforce an arbitrary value'
422 422
 				raise KeytabParsingError(message % index)
423
+		entry['spn'] = principal_to_spn(principal)
423 424
 	elif 'spn' in inentry:
425
+		entry['spn'] = inentry['spn']
424 426
 		entry['principal'] = spn_to_principal(inentry['spn'])
425 427
 
426 428
 def simple_kvno_to_full(inentry, index, entry, record):
... ...
@@ -439,6 +441,7 @@ def simple_timestamp_to_full(inentry, index, entry):
439 441
 	if 'timestamp' in inentry:
440 442
 		entry['timestamp'] = inentry['timestamp']
441 443
 	elif 'date' in inentry:
444
+		entry['date'] = inentry['date']
442 445
 		if inentry['date'] == 'now':
443 446
 			entry['timestamp'] = now
444 447
 		else:
... ...
@@ -452,6 +455,7 @@ def simple_enctype_to_full(inentry, index,  entry):
452 455
 	if 'enctype_raw' in inentry:
453 456
 		entry['enctype_raw'] = inentry['enctype_raw']
454 457
 	elif 'enctype' in inentry:
458
+		entry['enctype'] = inentry['enctype']
455 459
 		try:
456 460
 			entry['enctype_raw'] = ENC_TYPES[inentry['enctype']]
457 461
 		except KeyError:
... ...
@@ -467,13 +471,14 @@ def simple_keytab_to_full(indata):
467 471
 	}
468 472
 	for index, inentry in enumerate(indata.get('entries', [])):
469 473
 		entry = {}
470
-		record = {'entry': entry}
474
+		record = {'type': 'record', 'entry': entry, 'tail': b''}
471 475
 		simple_principal_to_full(inentry, index, entry)
472 476
 		simple_kvno_to_full(inentry, index, entry, record)
473 477
 		simple_timestamp_to_full(inentry, index, entry)
474 478
 		simple_enctype_to_full(inentry, index, entry)
475 479
 		if 'key' in inentry:
476 480
 			entry['key'] = inentry['key']
481
+			entry['key_length'] = len(unhexlify(inentry['key']))
477 482
 		data['records'].append(record)
478 483
 	return data
479 484
 
Browse code

Introduce --simple-to-full.

Xavier G authored on26/04/2020 18:24:15
Showing1 changed files
... ...
@@ -483,6 +483,8 @@ def yaml_to_keytab(buf, args):
483 483
 		# If the provided structure exposes "entries" rather than "records",
484 484
 		# then it very likely features the "simple" data layout.
485 485
 		data = simple_keytab_to_full(data)
486
+		if args.simple_to_full:
487
+			output_data(data, args, 0)
486 488
 	result = raw_keytab_to_binary(data)
487 489
 	sys.stdout.buffer.write(result)
488 490
 
... ...
@@ -492,6 +494,7 @@ def parse_args():
492 494
 	parser.add_argument('--data-layout', '-l',  dest='data_layout', choices=DATA_LAYOUTS.keys(), default='simple', help='data layout (keytab to YAML/JSON only)')
493 495
 	parser.add_argument('--output-format', '-f', dest='output_format', choices=['json', 'yaml'], default='yaml', help='output format (keytab to YAML/JSON only)')
494 496
 	parser.add_argument('--v1-endianness', '-e', dest='v1_endianness', choices=['native', 'little', 'big'], default='native', help='Enforce endianness (keytab v1 to YAML/JSON only)')
497
+	parser.add_argument('--simple-to-full', dest='simple_to_full', action='store_true', default=False, help='Convert from simple to full layout (YAML/JSON input only)')
495 498
 	parser.add_argument('input', nargs='?', type=argparse.FileType('rb'), default=sys.stdin.buffer, help='input file; defaults to standard input')
496 499
 	args = parser.parse_args()
497 500
 	return args
Browse code

Introduce output_data().

Xavier G authored on26/04/2020 18:23:59
Showing1 changed files
... ...
@@ -299,11 +299,16 @@ def keytab_data(buf, args):
299 299
 
300 300
 def keytab_to_yaml(buf, args):
301 301
 	keytab = keytab_data(buf, args)
302
-	final_keytab = prepare_serialization(keytab)
302
+	output_data(keytab, args, 0)
303
+
304
+def output_data(data, args, exit=None):
305
+	serialized_data = prepare_serialization(data)
303 306
 	if args.output_format == 'yaml':
304
-		yaml.dump(final_keytab, sys.stdout, width=160, sort_keys=False)
307
+		yaml.dump(serialized_data, sys.stdout, width=160, sort_keys=False)
305 308
 	else:
306
-		json.dump(final_keytab, sys.stdout, indent=4)
309
+		json.dump(serialized_data, sys.stdout, indent=4)
310
+	if exit is not None:
311
+		sys.exit(exit)
307 312
 
308 313
 def pack_data(data):
309 314
 	return struct.pack('>H', len(data)) + data
Browse code

Make kvno_in_tail a per-entry property.

Xavier G authored on26/04/2020 18:06:21
Showing1 changed files
... ...
@@ -226,10 +226,7 @@ def enrich_keytab(keytab):
226 226
 	"""
227 227
 	Enrich records with extra information suitable for human readers.
228 228
 	"""
229
-	# Reflect whether the keytab uses record tails to store 32-bits kvno:
230
-	records = keytab.pop('records')
231
-	keytab['kvno_in_tail'] = False
232
-	for record in records:
229
+	for record in keytab['records']:
233 230
 		if 'entry' not in record:
234 231
 			continue
235 232
 		entry = record['entry']
... ...
@@ -246,23 +243,17 @@ def enrich_keytab(keytab):
246 243
 			# those 32 bits is not 0. If present, this key version supersedes the 8-bit key version.
247 244
 			if len(record['tail']) >= 4:
248 245
 				tail_kvno = struct.unpack('>I', record['tail'][0:4])[0]
249
-				if tail_kvno:
250
-					entry['tail_kvno'] = tail_kvno
251
-					# tail_kvno overrides kvno if non-zero:
252
-					if entry['tail_kvno']:
253
-						entry['actual_kvno'] = entry['tail_kvno']
254
-						keytab['kvno_in_tail'] = True
246
+				if tail_kvno or not entry['kvno']:
247
+					entry['actual_kvno'] = entry['tail_kvno'] = tail_kvno
255 248
 			if 'actual_kvno' not in entry:
256 249
 				entry['actual_kvno'] = entry['kvno']
257
-	# Reintroduce records after kvno_in_tail for convenience:
258
-	keytab['records'] = records
259 250
 	return keytab
260 251
 
261 252
 def simplify_keytab(keytab):
262 253
 	"""
263 254
 	Simplify the keytab to make it suitable for edition.
264 255
 	"""
265
-	simplified = {'version': keytab['version'], 'kvno_in_tail': keytab['kvno_in_tail'], 'entries': []}
256
+	simplified = {'version': keytab['version'], 'entries': []}
266 257
 	for record in keytab['records']:
267 258
 		if 'entry' not in record:
268 259
 			continue
... ...
@@ -275,6 +266,8 @@ def simplify_keytab(keytab):
275 266
 			if key in entry['principal']:
276 267
 				simple_entry['principal'][key] = entry['principal'][key]
277 268
 		simple_entry['kvno'] = entry.get('actual_kvno', entry['kvno'])
269
+		if 'tail_kvno' in entry:
270
+			simple_entry['kvno_in_tail'] = True
278 271
 		for key in ('date', 'enctype', 'key'):
279 272
 			if key in entry:
280 273
 				simple_entry[key] = entry[key]
... ...
@@ -425,12 +418,12 @@ def simple_principal_to_full(inentry, index, entry):
425 418
 	elif 'spn' in inentry:
426 419
 		entry['principal'] = spn_to_principal(inentry['spn'])
427 420
 
428
-def simple_kvno_to_full(inentry, index, entry, record, kvno_in_tail=False):
421
+def simple_kvno_to_full(inentry, index, entry, record):
429 422
 	if 'kvno' in inentry:
430 423
 		entry['actual_kvno'] = inentry['kvno']
431 424
 		kvno_too_big = inentry['kvno'] > 255
432 425
 		entry['kvno'] = 0 if kvno_too_big else inentry['kvno']
433
-		if kvno_in_tail:
426
+		if inentry.get('kvno_in_tail', False):
434 427
 			entry['tail_kvno'] = inentry['kvno']
435 428
 			record['tail'] = hexlify(struct.pack('>I', inentry['kvno']))
436 429
 		elif kvno_too_big:
... ...
@@ -465,14 +458,13 @@ def simple_keytab_to_full(indata):
465 458
 	now = int(datetime.now().timestamp())
466 459
 	data = {
467 460
 		'version': indata.get('version', 2),
468
-		'kvno_in_tail': indata.get('kvno_in_tail', False),
469 461
 		'records': [],
470 462
 	}
471 463
 	for index, inentry in enumerate(indata.get('entries', [])):
472 464
 		entry = {}
473 465
 		record = {'entry': entry}
474 466
 		simple_principal_to_full(inentry, index, entry)
475
-		simple_kvno_to_full(inentry, index, entry, record, data['kvno_in_tail'])
467
+		simple_kvno_to_full(inentry, index, entry, record)
476 468
 		simple_timestamp_to_full(inentry, index, entry)
477 469
 		simple_enctype_to_full(inentry, index, entry)
478 470
 		if 'key' in inentry:
Browse code

Add support for simple YAML/JSON data layout.

The initial implementation used to support only raw and full YAML/JSON data
layout when converting YAML/JSON to keytab.

Xavier G authored on26/04/2020 16:33:32
Showing1 changed files
... ...
@@ -7,6 +7,7 @@
7 7
 
8 8
 from io import BufferedReader, BytesIO
9 9
 import os
10
+import re
10 11
 import sys
11 12
 import json
12 13
 import yaml
... ...
@@ -398,8 +399,93 @@ def raw_keytab_to_binary(indata):
398 399
 			raise KeytabComposingError('Unknown record type in record #%d' % index)
399 400
 	return data
400 401
 
402
+def spn_to_principal(spn, name_type='KRB5_NT_PRINCIPAL'):
403
+	principal = {'name_type': name_type}
404
+	principal['name_type_raw'] = NAME_TYPES[name_type]
405
+	rem = re.match(r'((?P<comp1>[^/]+)/)?(?P<comp2>[^:]+)(?::(?P<comp3>[^@]+))?@(?P<realm>.+)', spn)
406
+	if not rem:
407
+		raise KeytabComposingError('Cannot parse SPN %s into principal' % spn)
408
+	principal['realm'] = rem.group('realm')
409
+	principal['components'] = []
410
+	for name, component in rem.groupdict():
411
+		if name.startswith('comp') and component is not None:
412
+			principal['components'].append(component)
413
+	return principal
414
+
415
+def simple_principal_to_full(inentry, index, entry):
416
+	if 'principal' in inentry:
417
+		principal = entry['principal'] = inentry['principal']
418
+		if 'name_type_raw' not in principal:
419
+			try:
420
+				principal['name_type_raw'] = NAME_TYPES[principal['name_type']]
421
+			except KeyError:
422
+				message = 'Invalid or unknown name_type specified in entry #%d'
423
+				message += '; use name_type_raw to enforce an arbitrary value'
424
+				raise KeytabParsingError(message % index)
425
+	elif 'spn' in inentry:
426
+		entry['principal'] = spn_to_principal(inentry['spn'])
427
+
428
+def simple_kvno_to_full(inentry, index, entry, record, kvno_in_tail=False):
429
+	if 'kvno' in inentry:
430
+		entry['actual_kvno'] = inentry['kvno']
431
+		kvno_too_big = inentry['kvno'] > 255
432
+		entry['kvno'] = 0 if kvno_too_big else inentry['kvno']
433
+		if kvno_in_tail:
434
+			entry['tail_kvno'] = inentry['kvno']
435
+			record['tail'] = hexlify(struct.pack('>I', inentry['kvno']))
436
+		elif kvno_too_big:
437
+			message = 'Cannot store kvno > 255 without kvno_in_tail in entry #%d'
438
+			raise KeytabComposingError(message % index)
439
+
440
+def simple_timestamp_to_full(inentry, index, entry):
441
+	if 'timestamp' in inentry:
442
+		entry['timestamp'] = inentry['timestamp']
443
+	elif 'date' in inentry:
444
+		if inentry['date'] == 'now':
445
+			entry['timestamp'] = now
446
+		else:
447
+			try:
448
+				parsed_date = datetime.strptime(inentry['date'], DATE_TIME_FORMAT)
449
+				entry['timestamp'] = int(parsed_date.timestamp())
450
+			except:
451
+				raise KeytabParsingError('Invalid date specified in entry #%d' % index)
452
+
453
+def simple_enctype_to_full(inentry, index,  entry):
454
+	if 'enctype_raw' in inentry:
455
+		entry['enctype_raw'] = inentry['enctype_raw']
456
+	elif 'enctype' in inentry:
457
+		try:
458
+			entry['enctype_raw'] = ENC_TYPES[inentry['enctype']]
459
+		except KeyError:
460
+			message = 'Invalid or unknown enctype specified in entry #%d'
461
+			message += '; use enctype_raw to enforce an arbitrary value'
462
+			raise KeytabParsingError(message % index)
463
+
464
+def simple_keytab_to_full(indata):
465
+	now = int(datetime.now().timestamp())
466
+	data = {
467
+		'version': indata.get('version', 2),
468
+		'kvno_in_tail': indata.get('kvno_in_tail', False),
469
+		'records': [],
470
+	}
471
+	for index, inentry in enumerate(indata.get('entries', [])):
472
+		entry = {}
473
+		record = {'entry': entry}
474
+		simple_principal_to_full(inentry, index, entry)
475
+		simple_kvno_to_full(inentry, index, entry, record, data['kvno_in_tail'])
476
+		simple_timestamp_to_full(inentry, index, entry)
477
+		simple_enctype_to_full(inentry, index, entry)
478
+		if 'key' in inentry:
479
+			entry['key'] = inentry['key']
480
+		data['records'].append(record)
481
+	return data
482
+
401 483
 def yaml_to_keytab(buf, args):
402 484
 	data = yaml.load(buf, Loader=yaml.SafeLoader)
485
+	if 'entries' in data:
486
+		# If the provided structure exposes "entries" rather than "records",
487
+		# then it very likely features the "simple" data layout.
488
+		data = simple_keytab_to_full(data)
403 489
 	result = raw_keytab_to_binary(data)
404 490
 	sys.stdout.buffer.write(result)
405 491
 
Browse code

Introduce kvno_in_tail.

This extra property, shown only in full and simple modes, reflect whether
record tails are used to store 32-bit key version numbers.

Xavier G authored on26/04/2020 16:31:20
Showing1 changed files
... ...
@@ -225,7 +225,10 @@ def enrich_keytab(keytab):
225 225
 	"""
226 226
 	Enrich records with extra information suitable for human readers.
227 227
 	"""
228
-	for record in keytab['records']:
228
+	# Reflect whether the keytab uses record tails to store 32-bits kvno:
229
+	records = keytab.pop('records')
230
+	keytab['kvno_in_tail'] = False
231
+	for record in records:
229 232
 		if 'entry' not in record:
230 233
 			continue
231 234
 		entry = record['entry']
... ...
@@ -247,15 +250,18 @@ def enrich_keytab(keytab):
247 250
 					# tail_kvno overrides kvno if non-zero:
248 251
 					if entry['tail_kvno']:
249 252
 						entry['actual_kvno'] = entry['tail_kvno']
253
+						keytab['kvno_in_tail'] = True
250 254
 			if 'actual_kvno' not in entry:
251 255
 				entry['actual_kvno'] = entry['kvno']
256
+	# Reintroduce records after kvno_in_tail for convenience:
257
+	keytab['records'] = records
252 258
 	return keytab
253 259
 
254 260
 def simplify_keytab(keytab):
255 261
 	"""
256 262
 	Simplify the keytab to make it suitable for edition.
257 263
 	"""
258
-	simplified = {'version': keytab['version'], 'entries': []}
264
+	simplified = {'version': keytab['version'], 'kvno_in_tail': keytab['kvno_in_tail'], 'entries': []}
259 265
 	for record in keytab['records']:
260 266
 		if 'entry' not in record:
261 267
 			continue
Browse code

Adjust the way tail_kvno (vno32) overrides kvno.

Xavier G authored on26/04/2020 14:02:10
Showing1 changed files
... ...
@@ -244,8 +244,8 @@ def enrich_keytab(keytab):
244 244
 				tail_kvno = struct.unpack('>I', record['tail'][0:4])[0]
245 245
 				if tail_kvno:
246 246
 					entry['tail_kvno'] = tail_kvno
247
-					# If kvno is zero, assume the one found in the tail is the one that matters:
248
-					if not entry['kvno']:
247
+					# tail_kvno overrides kvno if non-zero:
248
+					if entry['tail_kvno']:
249 249
 						entry['actual_kvno'] = entry['tail_kvno']
250 250
 			if 'actual_kvno' not in entry:
251 251
 				entry['actual_kvno'] = entry['kvno']
Browse code

Implement conversion from YAML/JSON to keytab.

This first implementation only supports raw and full YAML/JSON data lyaout.

Xavier G authored on26/04/2020 12:20:12
Showing1 changed files
... ...
@@ -13,7 +13,7 @@ import yaml
13 13
 import struct
14 14
 import argparse
15 15
 from datetime import datetime
16
-from binascii import hexlify
16
+from binascii import hexlify, unhexlify
17 17
 
18 18
 # Documents used as reference to implement the keytab format:
19 19
 # [1] https://web.mit.edu/kerberos/krb5-1.12/doc/formats/keytab_file_format.html
... ...
@@ -91,6 +91,9 @@ class KeytabParsingError(Exception):
91 91
 	def __str__(self):
92 92
 		return __class__.MESSAGE.format(**self.__dict__, length=len(self.data))
93 93
 
94
+class KeytabComposingError(Exception):
95
+	pass
96
+
94 97
 def lookup(lookup_value, dictionary, default):
95 98
 	for name, value in dictionary.items():
96 99
 		if value == lookup_value:
... ...
@@ -302,9 +305,97 @@ def keytab_to_yaml(buf, args):
302 305
 	else:
303 306
 		json.dump(final_keytab, sys.stdout, indent=4)
304 307
 
305
-def yaml_to_keytab(fd):
306
-	data = yaml.load(fd.read(), Loader=yaml.SafeLoader)
307
-	print('YAML:', data)
308
+def pack_data(data):
309
+	return struct.pack('>H', len(data)) + data
310
+
311
+def pack_str(string, encoding=DEFAULT_ENCODING):
312
+	return pack_data(string.encode(encoding))
313
+
314
+def raw_principal_to_binary(principal, index):
315
+	for key in ('realm', 'components'):
316
+		if key not in principal:
317
+			raise KeytabComposingError('Mandatory key "%s" not found in principal #%d' % (key, index))
318
+	name_type_raw = principal.get('name_type_raw', NAME_TYPES['KRB5_NT_PRINCIPAL'])
319
+	try:
320
+		_ = int(name_type_raw)
321
+	except:
322
+		raise KeytabComposingError('invalid name_type_raw value in principal #%d' % index)
323
+	data = struct.pack('>H', len(principal['components']))
324
+	data += pack_str(principal['realm'])
325
+	for component in principal['components']:
326
+		data += pack_str(component)
327
+	data += struct.pack('>i', name_type_raw)
328
+	return data
329
+
330
+def raw_entry_to_binary(entry, index):
331
+	for key in ('principal', 'timestamp', 'kvno', 'enctype_raw', 'key'):
332
+		if key not in entry:
333
+			raise KeytabComposingError('Mandatory key "%s" not found in entry #%d' % (key, index))
334
+	for key in ('timestamp', 'kvno', 'enctype_raw'):
335
+		try:
336
+			assert(int(entry[key]) >= 0)
337
+		except:
338
+			raise KeytabComposingError('invalid %s data in entry #%d' % (key, index))
339
+	data = raw_principal_to_binary(entry['principal'], index)
340
+	try:
341
+		key_data = unhexlify(entry['key'])
342
+	except:
343
+		raise KeytabComposingError('invalid key data in entry #%d' % index)
344
+	data += struct.pack('>IBHH', entry['timestamp'], entry['kvno'], entry['enctype_raw'], len(key_data))
345
+	data += key_data
346
+	return data
347
+
348
+def raw_record_to_binary(record, index):
349
+	data = b''
350
+	if 'entry' not in record:
351
+		raise KeytabComposingError('missing entry in record #%d' % index)
352
+	data += raw_entry_to_binary(record['entry'], index)
353
+	if 'tail' in record:
354
+		try:
355
+			data += unhexlify(record['tail'])
356
+		except:
357
+			raise KeytabComposingError('invalid tail data in record #%d' % index)
358
+	return struct.pack('>i', len(data)) + data
359
+
360
+def raw_hole_to_binary(hole, index):
361
+	data = b''
362
+	if 'length' not in record:
363
+		raise KeytabComposingError('missing length in record #%d' % index)
364
+	try:
365
+		length = abs(int(record['length']))
366
+		if not length:
367
+			raise KeytabComposingError('illegal zero-length hole in record #%d' % index)
368
+		data += struct.pack('>i', -length)
369
+	except:
370
+		raise KeytabComposingError('invalid length in record #%d' % index)
371
+	if 'data' in record:
372
+		try:
373
+			hole_data = unhexlify(record['data'])
374
+		except:
375
+			raise KeytabComposingError('invalid data in record #%d' % index)
376
+		if len(hole_data) != length:
377
+			raise KeytabComposingError('length and data do not match in record #%d' % index)
378
+		data += hole_data
379
+	else:
380
+		data += b'\x00' * length
381
+	return data
382
+
383
+def raw_keytab_to_binary(indata):
384
+	data = bytes([KEYTAB_FIRST_BYTE, 2])
385
+	for index, record in enumerate(indata.get('records', [])):
386
+		record_type = record.get('type', 'record')
387
+		if record_type == 'hole':
388
+			data += raw_hole_to_binary(record, index)
389
+		elif record_type == 'record':
390
+			data += raw_record_to_binary(record, index)
391
+		else:
392
+			raise KeytabComposingError('Unknown record type in record #%d' % index)
393
+	return data
394
+
395
+def yaml_to_keytab(buf, args):
396
+	data = yaml.load(buf, Loader=yaml.SafeLoader)
397
+	result = raw_keytab_to_binary(data)
398
+	sys.stdout.buffer.write(result)
308 399
 
309 400
 def parse_args():
310 401
 	parser = argparse.ArgumentParser(description='Keytab <-> YAML/JSON convertor.')
Browse code

Implement keytabv1 format. This is completely untested though.

Xavier G authored on25/04/2020 17:57:30
Showing1 changed files
... ...
@@ -30,6 +30,8 @@ DATE_TIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
30 30
 KEYTAB_FIRST_BYTE = 0x05
31 31
 # Default prefix for struct's format strings, defining big-endian byte order:
32 32
 BIG_ENDIAN='>'
33
+LITTLE_ENDIAN='<'
34
+NATIVE_ENDIANNESS='='
33 35
 DEFAULT_PREFIX=BIG_ENDIAN
34 36
 DEFAULT_ENCODING='ascii'
35 37
 VERBOSITY=1
... ...
@@ -102,7 +104,7 @@ def int_to_enc_type(lookup_value):
102 104
 	return lookup(lookup_value, ENC_TYPES, 'ENCTYPE_UNKNOWN')
103 105
 
104 106
 def principal_to_spn(principal):
105
-	if principal['name_type_raw'] != 1:
107
+	if principal.get('name_type_raw') != NAME_TYPES['KRB5_NT_PRINCIPAL']:
106 108
 		return None
107 109
 	components = principal['components']
108 110
 	count = len(components)
... ...
@@ -145,29 +147,34 @@ def parse_data(buf, prefix=DEFAULT_PREFIX):
145 147
 def parse_str(buf, prefix=DEFAULT_PREFIX, encoding=DEFAULT_ENCODING):
146 148
 	return parse_data(buf, prefix).decode(encoding)
147 149
 
148
-def parse_principal(buf, prefix=DEFAULT_PREFIX):
150
+def parse_principal(buf, prefix=DEFAULT_PREFIX, version=2):
149 151
 	principal = {}
150 152
 	# [1] states "count of components (32 bits)" but [2] says int16:
151 153
 	component_count = unpack(buf, prefix, 'H')
154
+	# [1] states "[includes realm in version 1]"
155
+	if version == 1:
156
+		component_count -= 1
152 157
 	principal['realm'] = parse_str(buf, prefix)
153 158
 	components = []
154 159
 	for i in range(component_count):
155 160
 		components.append(parse_str(buf, prefix))
156 161
 	principal['components'] = components
157
-	# [3] states int32:
158
-	principal['name_type_raw'] = unpack(buf, prefix, 'i')
162
+	# [1] states "[omitted in version 1]"
163
+	if version != 1:
164
+		# [3] states int32:
165
+		principal['name_type_raw'] = unpack(buf, prefix, 'i')
159 166
 	return principal
160 167
 
161
-def parse_entry(buf, prefix=DEFAULT_PREFIX):
168
+def parse_entry(buf, prefix=DEFAULT_PREFIX, version=2):
162 169
 	entry = {}
163
-	entry['principal'] = parse_principal(buf, prefix)
170
+	entry['principal'] = parse_principal(buf, prefix, version)
164 171
 	entry['timestamp'], entry['kvno'], entry['enctype_raw'], entry['key_length'] = unpack(buf, prefix, 'IBHH')
165 172
 	entry['key'] = buf.read(entry['key_length'])
166 173
 	return entry
167 174
 
168
-def parse_record(buf, prefix=DEFAULT_PREFIX):
175
+def parse_record(buf, prefix=DEFAULT_PREFIX, version=2):
169 176
 	record = {'type': 'record'}
170
-	record['entry'] = parse_entry(buf, prefix)
177
+	record['entry'] = parse_entry(buf, prefix, version)
171 178
 	record['tail'] = buf.read()
172 179
 	return record
173 180
 
... ...
@@ -175,8 +182,12 @@ def parse_keytab(buf, args):
175 182
 	second_byte = buf.read(2)[1]
176 183
 	verbose(2, 'keytab v{}', second_byte)
177 184
 	if second_byte == 1:
178
-		verbose(1, 'Keytab v1 not supported yet!')
179
-		sys.exit(1)
185
+		# Version 1 uses native byte order:
186
+		prefix = NATIVE_ENDIANNESS
187
+		if args.v1_endianness == 'big':
188
+			prefix = BIG_ENDIAN
189
+		elif args.v1_endianness == 'little':
190
+			prefix = LITTLE_ENDIAN
180 191
 	elif second_byte == 2:
181 192
 		# Version 2 always uses big-endian byte order:
182 193
 		prefix = BIG_ENDIAN
... ...
@@ -200,7 +211,7 @@ def parse_keytab(buf, args):
200 211
 		verbose(3, 'Record #{} of length {}', len(keytab['records']) + 1, record_length)
201 212
 		record = buf.read(abs(record_length))
202 213
 		if record_length > 0:
203
-			record = parse_record(BufferedReader(BytesIO(record)), prefix)
214
+			record = parse_record(BufferedReader(BytesIO(record)), prefix, second_byte)
204 215
 		else:
205 216
 			record = {'type': 'hole', 'data': record}
206 217
 		record['length'] = record_length
... ...
@@ -300,6 +311,7 @@ def parse_args():
300 311
 	parser.add_argument('--verbose', '-v', dest='verbose', action='count', help='increase verbosity level', default=VERBOSITY)
301 312
 	parser.add_argument('--data-layout', '-l',  dest='data_layout', choices=DATA_LAYOUTS.keys(), default='simple', help='data layout (keytab to YAML/JSON only)')
302 313
 	parser.add_argument('--output-format', '-f', dest='output_format', choices=['json', 'yaml'], default='yaml', help='output format (keytab to YAML/JSON only)')
314
+	parser.add_argument('--v1-endianness', '-e', dest='v1_endianness', choices=['native', 'little', 'big'], default='native', help='Enforce endianness (keytab v1 to YAML/JSON only)')
303 315
 	parser.add_argument('input', nargs='?', type=argparse.FileType('rb'), default=sys.stdin.buffer, help='input file; defaults to standard input')
304 316
 	args = parser.parse_args()
305 317
 	return args
Browse code

Add first lumps of code.

At this stage, yamltab can parse keytabs (version 2 format only) and drop their
contents as either YAML or JSON, with three possible data layouts.

Xavier G authored on25/04/2020 15:10:02
Showing1 changed files
1 1
new file mode 100755
... ...
@@ -0,0 +1,319 @@
1
+#!/usr/bin/env python3
2
+
3
+# Copyright © 2020 Xavier G. <xavier.yamltab@kindwolf.org>
4
+# This work is free. You can redistribute it and/or modify it under the
5
+# terms of the Do What The Fuck You Want To Public License, Version 2,
6
+# as published by Sam Hocevar. See the COPYING file for more details.
7
+
8
+from io import BufferedReader, BytesIO
9
+import os
10
+import sys
11
+import json
12
+import yaml
13
+import struct
14
+import argparse
15
+from datetime import datetime
16
+from binascii import hexlify
17
+
18
+# Documents used as reference to implement the keytab format:
19
+# [1] https://web.mit.edu/kerberos/krb5-1.12/doc/formats/keytab_file_format.html
20
+# [2] https://github.com/krb5/krb5/blob/master/src/lib/krb5/keytab/kt_file.c#L892
21
+# [3] https://github.com/krb5/krb5/blob/master/src/include/krb5/krb5.hin#L230
22
+
23
+DATA_LAYOUT_RAW = 0
24
+DATA_LAYOUT_FULL = 1
25
+DATA_LAYOUT_SIMPLE = 2
26
+DATA_LAYOUTS = {'raw': DATA_LAYOUT_RAW, 'full': DATA_LAYOUT_FULL, 'simple': DATA_LAYOUT_SIMPLE}
27
+
28
+DATE_TIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
29
+
30
+KEYTAB_FIRST_BYTE = 0x05
31
+# Default prefix for struct's format strings, defining big-endian byte order:
32
+BIG_ENDIAN='>'
33
+DEFAULT_PREFIX=BIG_ENDIAN
34
+DEFAULT_ENCODING='ascii'
35
+VERBOSITY=1
36
+CALCSIZE={}
37
+
38
+# The following table is based on [3]:
39
+NAME_TYPES = {
40
+	'KRB5_NT_UNKNOWN': 0,
41
+	'KRB5_NT_PRINCIPAL': 1,
42
+	'KRB5_NT_SRV_INST': 2,
43
+	'KRB5_NT_SRV_HST': 3,
44
+	'KRB5_NT_SRV_XHST': 4,
45
+	'KRB5_NT_UID': 5,
46
+	'KRB5_NT_X500_PRINCIPAL': 6,
47
+	'KRB5_NT_SMTP_NAME': 7,
48
+	'KRB5_NT_ENTERPRISE_PRINCIPAL': 10,
49
+	'KRB5_NT_WELLKNOWN': 11,
50
+	'KRB5_NT_MS_PRINCIPAL': -128,
51
+	'KRB5_NT_MS_PRINCIPAL_AND_ID': -129,
52
+	'KRB5_NT_ENT_PRINCIPAL_AND_ID': -130,
53
+}
54
+
55
+ENC_TYPES = {
56
+	'NULL': 0,
57
+	'DES_CBC_CRC': 1,
58
+	'DES_CBC_MD4': 2,
59
+	'DES_CBC_MD5': 3,
60
+	'DES_CBC_RAW': 4,
61
+	'DES3_CBC_SHA': 5,
62
+	'DES3_CBC_RAW': 6,
63
+	'DES_HMAC_SHA1': 8,
64
+	'DSA_SHA1_CMS': 9,
65
+	'MD5_RSA_CMS': 10,
66
+	'SHA1_RSA_CMS': 11,
67
+	'RC2_CBC_ENV': 12,
68
+	'RSA_ENV': 13,
69
+	'RSA_ES_OAEP_ENV': 14,
70
+	'DES3_CBC_ENV': 15,
71
+	'DES3_CBC_SHA1': 16,
72
+	'AES128_CTS_HMAC_SHA1_96': 17,
73
+	'AES256_CTS_HMAC_SHA1_96': 18,
74
+	'AES128_CTS_HMAC_SHA256_128': 19,
75
+	'AES256_CTS_HMAC_SHA384_192': 20,
76
+	'ARCFOUR_HMAC': 23,
77
+	'ARCFOUR_HMAC_EXP': 24,
78
+	'CAMELLIA128_CTS_CMAC': 25,
79
+	'CAMELLIA256_CTS_CMAC': 26,
80
+	'UNKNOWN': 511,
81
+}
82
+
83
+class KeytabParsingError(Exception):
84
+	MESSAGE = 'Parsing eror: expected {size} bytes to unpack {format} but read {length} bytes instead: {data}'
85
+	def __init__(self, data, size, frmt):
86
+		self.data = data
87
+		self.size = size
88
+		self.format = frmt
89
+	def __str__(self):
90
+		return __class__.MESSAGE.format(**self.__dict__, length=len(self.data))
91
+
92
+def lookup(lookup_value, dictionary, default):
93
+	for name, value in dictionary.items():
94
+		if value == lookup_value:
95
+			return name
96
+	return default
97
+
98
+def int_to_name_type(lookup_value):
99
+	return lookup(lookup_value, NAME_TYPES, 'KRB5_NT_UNKNOWN')
100
+
101
+def int_to_enc_type(lookup_value):
102
+	return lookup(lookup_value, ENC_TYPES, 'ENCTYPE_UNKNOWN')
103
+
104
+def principal_to_spn(principal):
105
+	if principal['name_type_raw'] != 1:
106
+		return None
107
+	components = principal['components']
108
+	count = len(components)
109
+	if count < 1 or count > 3:
110
+		return None
111
+	for component in components:
112
+		if not component:
113
+			return None
114
+	spn = components[0]
115
+	if count >= 2:
116
+		spn += '/' + components[1]
117
+	if count == 3:
118
+		spn += ':' + components[2]
119
+	spn += '@' + principal['realm']
120
+	return spn
121
+
122
+def verbose(level, msg, *args, **kwargs):
123
+	if level <= VERBOSITY:
124
+		message = msg.format(*args, **kwargs)
125
+		sys.stderr.write(message + '\n')
126
+
127
+def unpack(buf, prefix, format):
128
+	"""
129
+	Wrapper around read(), struct.unpack() and struct.calcsize().
130
+	"""
131
+	actual_format = prefix + format
132
+	size = CALCSIZE.get(actual_format)
133
+	if size is None:
134
+		size = CALCSIZE[actual_format] = struct.calcsize(actual_format)
135
+	data = buf.read(size)
136
+	if len(data) < size:
137
+		raise KeytabParsingError(data, size, actual_format)
138
+	results = struct.unpack(actual_format, data)
139
+	return results[0] if len(results) == 1 else results
140
+
141
+def parse_data(buf, prefix=DEFAULT_PREFIX):
142
+	length = unpack(buf, prefix, 'H')
143
+	return buf.read(length)
144
+
145
+def parse_str(buf, prefix=DEFAULT_PREFIX, encoding=DEFAULT_ENCODING):
146
+	return parse_data(buf, prefix).decode(encoding)
147
+
148
+def parse_principal(buf, prefix=DEFAULT_PREFIX):
149
+	principal = {}
150
+	# [1] states "count of components (32 bits)" but [2] says int16:
151
+	component_count = unpack(buf, prefix, 'H')
152
+	principal['realm'] = parse_str(buf, prefix)
153
+	components = []
154
+	for i in range(component_count):
155
+		components.append(parse_str(buf, prefix))
156
+	principal['components'] = components
157
+	# [3] states int32:
158
+	principal['name_type_raw'] = unpack(buf, prefix, 'i')
159
+	return principal
160
+
161
+def parse_entry(buf, prefix=DEFAULT_PREFIX):
162
+	entry = {}
163
+	entry['principal'] = parse_principal(buf, prefix)
164
+	entry['timestamp'], entry['kvno'], entry['enctype_raw'], entry['key_length'] = unpack(buf, prefix, 'IBHH')
165
+	entry['key'] = buf.read(entry['key_length'])
166
+	return entry
167
+
168
+def parse_record(buf, prefix=DEFAULT_PREFIX):
169
+	record = {'type': 'record'}
170
+	record['entry'] = parse_entry(buf, prefix)
171
+	record['tail'] = buf.read()
172
+	return record
173
+
174
+def parse_keytab(buf, args):
175
+	second_byte = buf.read(2)[1]
176
+	verbose(2, 'keytab v{}', second_byte)
177
+	if second_byte == 1:
178
+		verbose(1, 'Keytab v1 not supported yet!')
179
+		sys.exit(1)
180
+	elif second_byte == 2:
181
+		# Version 2 always uses big-endian byte order:
182
+		prefix = BIG_ENDIAN
183
+	else:
184
+		verbose(1, 'Unknown keytab version: v{}', second_byte)
185
+		sys.exit(1)
186
+
187
+	keytab = {
188
+		'version': second_byte,
189
+		'records': [],
190
+	}
191
+	while True:
192
+		try:
193
+			record_length = unpack(buf, prefix, 'i')
194
+		except KeytabParsingError as kpe:
195
+			if len(kpe.data):
196
+				verbose(1, 'Premature end of file? Got {} as record length.', kpe.data)
197
+			break
198
+		if not record_length:
199
+			break
200
+		verbose(3, 'Record #{} of length {}', len(keytab['records']) + 1, record_length)
201
+		record = buf.read(abs(record_length))
202
+		if record_length > 0:
203
+			record = parse_record(BufferedReader(BytesIO(record)), prefix)
204
+		else:
205
+			record = {'type': 'hole', 'data': record}
206
+		record['length'] = record_length
207
+		keytab['records'].append(record)
208
+	return keytab
209
+
210
+def enrich_keytab(keytab):
211
+	"""
212
+	Enrich records with extra information suitable for human readers.
213
+	"""
214
+	for record in keytab['records']:
215
+		if 'entry' not in record:
216
+			continue
217
+		entry = record['entry']
218
+		entry['date'] = datetime.utcfromtimestamp(entry['timestamp']).strftime(DATE_TIME_FORMAT)
219
+		if 'name_type_raw' in entry['principal']:
220
+			entry['principal']['name_type'] = int_to_name_type(entry['principal']['name_type_raw'])
221
+		spn = principal_to_spn(entry['principal'])
222
+		if spn:
223
+			entry['spn'] = spn
224
+		entry['enctype'] = int_to_enc_type(entry['enctype_raw'])
225
+		if 'tail' in record:
226
+			# [1] states: Some implementations of Kerberos recognize a 32-bit key version at the end of
227
+			# an entry, if the record length is at least 4 bytes longer than the entry and the value of
228
+			# those 32 bits is not 0. If present, this key version supersedes the 8-bit key version.
229
+			if len(record['tail']) >= 4:
230
+				tail_kvno = struct.unpack('>I', record['tail'][0:4])[0]
231
+				if tail_kvno:
232
+					entry['tail_kvno'] = tail_kvno
233
+					# If kvno is zero, assume the one found in the tail is the one that matters:
234
+					if not entry['kvno']:
235
+						entry['actual_kvno'] = entry['tail_kvno']
236
+			if 'actual_kvno' not in entry:
237
+				entry['actual_kvno'] = entry['kvno']
238
+	return keytab
239
+
240
+def simplify_keytab(keytab):
241
+	"""
242
+	Simplify the keytab to make it suitable for edition.
243
+	"""
244
+	simplified = {'version': keytab['version'], 'entries': []}
245
+	for record in keytab['records']:
246
+		if 'entry' not in record:
247
+			continue
248
+		entry = record['entry']
249
+		simple_entry = {}
250
+		if 'spn' in entry:
251
+			simple_entry['spn'] = entry['spn']
252
+		simple_entry['principal'] = {}
253
+		for key in ('name_type', 'components', 'realm'):
254
+			if key in entry['principal']:
255
+				simple_entry['principal'][key] = entry['principal'][key]
256
+		simple_entry['kvno'] = entry.get('actual_kvno', entry['kvno'])
257
+		for key in ('date', 'enctype', 'key'):
258
+			if key in entry:
259
+				simple_entry[key] = entry[key]
260
+		simplified['entries'].append(simple_entry)
261
+	return simplified
262
+
263
+def prepare_serialization(obj):
264
+	"""
265
+	Prepare keytab for serialization.
266
+	"""
267
+	if type(obj) is dict:
268
+		for key, value in obj.items():
269
+			obj[key] = prepare_serialization(value)
270
+	elif type(obj) is list:
271
+		for index, value in enumerate(obj):
272
+			obj[index] = prepare_serialization(value)
273
+	elif type(obj) is bytes:
274
+		obj = hexlify(obj).decode(DEFAULT_ENCODING)
275
+	return obj
276
+
277
+def keytab_data(buf, args):
278
+	keytab = parse_keytab(buf, args)
279
+	layout = DATA_LAYOUTS.get(args.data_layout, DATA_LAYOUT_FULL)
280
+	if layout >= DATA_LAYOUT_FULL:
281
+		keytab = enrich_keytab(keytab)
282
+	if layout >= DATA_LAYOUT_SIMPLE:
283
+		keytab = simplify_keytab(keytab)
284
+	return keytab
285
+
286
+def keytab_to_yaml(buf, args):
287
+	keytab = keytab_data(buf, args)
288
+	final_keytab = prepare_serialization(keytab)
289
+	if args.output_format == 'yaml':
290
+		yaml.dump(final_keytab, sys.stdout, width=160, sort_keys=False)
291
+	else:
292
+		json.dump(final_keytab, sys.stdout, indent=4)
293
+
294
+def yaml_to_keytab(fd):
295
+	data = yaml.load(fd.read(), Loader=yaml.SafeLoader)
296
+	print('YAML:', data)
297
+
298
+def parse_args():
299
+	parser = argparse.ArgumentParser(description='Keytab <-> YAML/JSON convertor.')
300
+	parser.add_argument('--verbose', '-v', dest='verbose', action='count', help='increase verbosity level', default=VERBOSITY)
301
+	parser.add_argument('--data-layout', '-l',  dest='data_layout', choices=DATA_LAYOUTS.keys(), default='simple', help='data layout (keytab to YAML/JSON only)')
302
+	parser.add_argument('--output-format', '-f', dest='output_format', choices=['json', 'yaml'], default='yaml', help='output format (keytab to YAML/JSON only)')
303
+	parser.add_argument('input', nargs='?', type=argparse.FileType('rb'), default=sys.stdin.buffer, help='input file; defaults to standard input')
304
+	args = parser.parse_args()
305
+	return args
306
+
307
+def main():
308
+	args = parse_args()
309
+	global VERBOSITY
310
+	VERBOSITY=args.verbose
311
+	buf = args.input
312
+	first_byte = buf.peek(1)[0]
313
+	if first_byte == KEYTAB_FIRST_BYTE:
314
+		keytab_to_yaml(buf, args)
315
+	else:
316
+		yaml_to_keytab(buf, args)
317
+
318
+if __name__ == '__main__':
319
+	main()