Hide keyboard shortcuts

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. 

5 

6import os 

7from collections import deque 

8 

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") 

16 

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") 

20 

21 

22def xor(s1,s2): 

23 return b"".join(b"%c" % (a ^ b) for a,b in zip(s1,s2)) 

24 

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 

40 

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 

56 

57 

58class SysAuthenticatorBase: 

59 USED_SALT = deque(maxlen=USED_SALT_CACHE_SIZE) 

60 

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) 

76 

77 def get_uid(self) -> int: 

78 raise NotImplementedError() 

79 

80 def get_gid(self) -> int: 

81 raise NotImplementedError() 

82 

83 def requires_challenge(self) -> bool: 

84 return True 

85 

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 

94 

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 () 

100 

101 def get_password(self): 

102 return None 

103 

104 def check(self, _password) -> bool: 

105 return False 

106 

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 

113 

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) 

131 

132 def choose_salt_digest(self, digest_modes): 

133 self.salt_digest = choose_digest(digest_modes) 

134 return self.salt_digest 

135 

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 

148 

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 

170 

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 

191 

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 

212 

213 

214class SysAuthenticator(SysAuthenticatorBase): 

215 

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) 

225 

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 

230 

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