Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/server/auth/sys_auth_base.py : 62%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of Xpra.
2# Copyright (C) 2013-2020 Antoine Martin <antoine@xpra.org>
3# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
4# later version. See the file COPYING for details.
6import os
7from collections import deque
9from xpra.platform.dotxpra import DotXpra
10from xpra.platform.paths import get_socket_dirs
11from xpra.util import envint, obsc, typedict
12from xpra.net.digest import get_salt, choose_digest, verify_digest, gendigest
13from xpra.os_util import hexstr, POSIX
14from xpra.log import Logger
15log = Logger("auth")
17USED_SALT_CACHE_SIZE = envint("XPRA_USED_SALT_CACHE_SIZE", 1024*1024)
18DEFAULT_UID = os.environ.get("XPRA_AUTHENTICATION_DEFAULT_UID", "nobody")
19DEFAULT_GID = os.environ.get("XPRA_AUTHENTICATION_DEFAULT_GID", "nobody")
22def xor(s1,s2):
23 return b"".join(b"%c" % (a ^ b) for a,b in zip(s1,s2))
25def parse_uid(v) -> int:
26 if v:
27 try:
28 return int(v)
29 except (TypeError, ValueError):
30 log("uid '%s' is not an int", v)
31 if POSIX:
32 try:
33 import pwd
34 return pwd.getpwnam(v or DEFAULT_UID).pw_uid
35 except Exception as e:
36 log("parse_uid(%s)", v, exc_info=True)
37 log.error("Error: cannot find uid of '%s': %s", v, e)
38 return os.getuid()
39 return -1
41def parse_gid(v) -> int:
42 if v:
43 try:
44 return int(v)
45 except (TypeError, ValueError):
46 log("gid '%s' is not an int", v)
47 if POSIX:
48 try:
49 import grp #@UnresolvedImport
50 return grp.getgrnam(v or DEFAULT_GID).gr_gid
51 except Exception as e:
52 log("parse_gid(%s)", v, exc_info=True)
53 log.error("Error: cannot find gid of '%s': %s", v, e)
54 return os.getgid()
55 return -1
58class SysAuthenticatorBase:
59 USED_SALT = deque(maxlen=USED_SALT_CACHE_SIZE)
61 def __init__(self, username, **kwargs):
62 self.username = username
63 self.salt = None
64 self.digest = None
65 self.salt_digest = None
66 self.prompt = kwargs.pop("prompt", "password")
67 self.socket_dirs = kwargs.pop("socket-dirs", get_socket_dirs())
68 self.challenge_sent = False
69 self.passed = False
70 self.password_used = None
71 #warn about unused options:
72 unused = dict((k,v) for k,v in kwargs.items() if k not in ("connection", "exec_cwd"))
73 if unused:
74 log.warn("Warning: unused keyword arguments for %s authentication:", self)
75 log.warn(" %s", unused)
77 def get_uid(self) -> int:
78 raise NotImplementedError()
80 def get_gid(self) -> int:
81 raise NotImplementedError()
83 def requires_challenge(self) -> bool:
84 return True
86 def get_challenge(self, digests):
87 if self.salt is not None:
88 log.error("Error: authentication challenge already sent!")
89 return None
90 self.salt = get_salt()
91 self.digest = choose_digest(digests)
92 self.challenge_sent = True
93 return self.salt, self.digest
95 def get_passwords(self):
96 p = self.get_password() #pylint: disable=assignment-from-none
97 if p is not None:
98 return (p,)
99 return ()
101 def get_password(self):
102 return None
104 def check(self, _password) -> bool:
105 return False
107 def authenticate(self, caps : typedict) -> bool:
108 r = self.do_authenticate(caps)
109 if r:
110 self.passed = True
111 log("authentication challenge passed for %s", self)
112 return r
114 def do_authenticate(self, caps : typedict) -> bool:
115 if self.passed:
116 log("invalid state: challenge has already been passed")
117 return False
118 if not caps:
119 log("invalid state: no capabilities")
120 return False
121 if not self.challenge_sent:
122 log("invalid state: challenge has not been sent yet!")
123 return False
124 challenge_response = caps.strget("challenge_response")
125 client_salt = caps.strget("challenge_client_salt")
126 #challenge has been sent already for this module
127 if not challenge_response:
128 log("invalid state: challenge already sent but no response found!")
129 return False
130 return self.authenticate_check(challenge_response, client_salt)
132 def choose_salt_digest(self, digest_modes):
133 self.salt_digest = choose_digest(digest_modes)
134 return self.salt_digest
136 def get_response_salt(self, client_salt=None):
137 server_salt = self.salt
138 #make sure it does not get re-used:
139 self.salt = None
140 if client_salt is None:
141 return server_salt
142 salt = gendigest(self.salt_digest, client_salt, server_salt)
143 if salt in SysAuthenticator.USED_SALT:
144 raise Exception("danger: an attempt was made to re-use the same computed salt")
145 log("combined salt(%s, %s)=%s", hexstr(server_salt), hexstr(client_salt), hexstr(salt))
146 SysAuthenticator.USED_SALT.append(salt)
147 return salt
149 def authenticate_check(self, challenge_response, client_salt=None) -> bool:
150 if self.salt is None:
151 log.error("Error: illegal challenge response received - salt cleared or unset")
152 return False
153 salt = self.get_response_salt(client_salt)
154 password = gendigest("xor", challenge_response, salt)
155 log("authenticate_check(%s, %s) response salt=%s",
156 obsc(repr(challenge_response)), repr(client_salt), repr(salt))
157 #warning: enabling logging here would log the actual system password!
158 #log.info("authenticate(%s, %s) password=%s (%s)",
159 # hexstr(challenge_response), hexstr(client_salt), password, hexstr(password))
160 #verify login:
161 try :
162 ret = self.check(password)
163 log("authenticate_check(..)=%s", ret)
164 except Exception as e:
165 log("check(..)", exc_info=True)
166 log.error("Error: %s authentication check failed:", self)
167 log.error(" %s", e)
168 return False
169 return ret
171 def authenticate_hmac(self, challenge_response, client_salt=None) -> bool:
172 log("sys_auth_base.authenticate_hmac(%r, %r)", challenge_response, client_salt)
173 if not self.salt:
174 log.error("Error: illegal challenge response received - salt cleared or unset")
175 return None
176 salt = self.get_response_salt(client_salt)
177 passwords = self.get_passwords()
178 if not passwords:
179 log.warn("Warning: %s authentication failed", self)
180 log.warn(" no password defined for '%s'", self.username)
181 return False
182 log("found %i passwords using %r", len(passwords), self)
183 for x in passwords:
184 if verify_digest(self.digest, x, salt, challenge_response):
185 self.password_used = x
186 return True
187 log.warn("Warning: %s challenge for '%s' does not match", self.digest, self.username)
188 if len(passwords)>1:
189 log.warn(" checked %i passwords", len(passwords))
190 return False
192 def get_sessions(self):
193 uid = self.get_uid()
194 gid = self.get_gid()
195 log("%s.get_sessions() uid=%i, gid=%i", self, uid, gid)
196 try:
197 sockdir = DotXpra(None, self.socket_dirs, actual_username=self.username, uid=uid, gid=gid)
198 results = sockdir.sockets(check_uid=uid)
199 displays = []
200 for state, display in results:
201 if state==DotXpra.LIVE and display not in displays:
202 displays.append(display)
203 log("sockdir=%s, results=%s, displays=%s", sockdir, results, displays)
204 except Exception as e:
205 log("get_sessions()", exc_info=True)
206 log.error("Error: cannot get the list of sessions for '%s':", self.username)
207 log.error(" %s", e)
208 displays = []
209 v = uid, gid, displays, {}, {}
210 log("%s.get_sessions()=%s", self, v)
211 return v
214class SysAuthenticator(SysAuthenticatorBase):
216 def __init__(self, username, **kwargs):
217 super().__init__(username)
218 self.pw = None
219 if POSIX:
220 try:
221 import pwd
222 self.pw = pwd.getpwnam(username)
223 except Exception:
224 log("cannot load password database entry for '%s'", username, exc_info=True)
226 def get_uid(self) -> int:
227 if self.pw is None:
228 raise Exception("username '%s' not found" % self.username)
229 return self.pw.pw_uid
231 def get_gid(self) -> int:
232 if self.pw is None:
233 raise Exception("username '%s' not found" % self.username)
234 return self.pw.pw_gid