File: hash_passwords.py

package info (click to toggle)
ddnet 19.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 68,960 kB
  • sloc: cpp: 195,050; ansic: 58,572; python: 5,568; asm: 946; sh: 941; java: 366; xml: 206; makefile: 31
file content (85 lines) | stat: -rw-r--r-- 3,165 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import argparse
import tempfile
import binascii
import hashlib
import os
import re
import sys

AUTH_ADD_REGEX=re.compile(r'^\s*auth_add\s+(?P<username>"[^"]*"|[^"\s]+)\s+(?P<level>"[^"]*"|[^"\s]+)\s+(?P<password>"[^"]*"|[^"\s]+)\s*$')
AUTH_ADD_PRESENT_REGEX=re.compile(r'(^|\W)auth_add($|\W)')

def hash_password(password):
	salt = os.urandom(8)
	h = hashlib.md5()
	h.update(password.encode())
	h.update(salt)
	return h.hexdigest(), binascii.hexlify(salt).decode('ascii')

def auth_add_p_line(username, level, pwhash, salt):
	if level not in ('admin', 'mod', 'moderator', 'helper'):
		print(f"Warning: level ({level}) not one of admin, mod or helper.", file=sys.stderr)
	if repr(level) != f"'{level}'":
		print(f"Warning: level ({level}) contains weird symbols, config line is possibly malformed.", file=sys.stderr)
	if repr(username) != f"'{username}'":
		print(f"Warning: username ({username}) contains weird symbols, config line is possibly malformed.", file=sys.stderr)
	username = username.replace('"', '\\"')
	if ' ' in username or ';' in username:
		username = f'"{username}"'
	return f"auth_add_p {username} {level} {pwhash} {salt}"

def auth_add_p_line_from_pw(username, level, password):
	if len(password) < 8:
		print("Warning: password too short for a long-term password.", file=sys.stderr)
	pwhash, salt = hash_password(password)
	return auth_add_p_line(username, level, pwhash, salt)

def parse_line(line):
	m = AUTH_ADD_REGEX.match(line)
	if not m:
		if AUTH_ADD_PRESENT_REGEX.search(line):
			print("Warning: Funny-looking line with 'auth_add', not touching it:")
			print(line, end="")
		return None
	password = m.group('password')
	if password.startswith('"'):
		password = password[1:-1] # Strip quotes.
	return m.group('username'), m.group('level'), password

def main():
	p = argparse.ArgumentParser(description="Hash passwords in a way suitable for DDNet configs.")
	p.add_argument('--new', '-n', nargs=3, metavar=("USERNAME", "LEVEL", "PASSWORD"), action='append', default=[], help="username, level and password of the new user")
	p.add_argument('config', nargs='?', metavar="CONFIG", help="config file to update.")
	args = p.parse_args()
	if not args.new and args.config is None:
		p.error("expected at least one of --new and CONFIG")

	use_stdio = args.config is None or args.config == '-'
	if use_stdio:
		if args.config is None:
			input_file = open(os.devnull, encoding="utf-8")
		else:
			input_file = sys.stdin
		output_file = sys.stdout
	else:
		input_file = open(args.config, encoding="utf-8") # pylint: disable=consider-using-with
		output_file = tempfile.NamedTemporaryFile('w', dir=os.getcwd(), prefix=f"{args.config}.", delete=False) # pylint: disable=consider-using-with

	for line in input_file:
		parsed = parse_line(line)
		if parsed is None:
			print(line, end="", file=output_file)
		else:
			print(auth_add_p_line_from_pw(*parsed), file=output_file)

	for auth_tuple in args.new:
		print(auth_add_p_line_from_pw(*auth_tuple), file=output_file)

	if not use_stdio:
		input_file.close()
		output_filename = output_file.name
		output_file.close()
		os.rename(output_filename, args.config)

if __name__ == '__main__':
	main()