xpra icon
Bug tracker and wiki

This bug tracker and wiki are being discontinued
please use https://github.com/Xpra-org/xpra instead.


Ticket #426: auth-v3.patch

File auth-v3.patch, 45.5 KB (added by Antoine Martin, 8 years ago)

splits authentication from server core, adds auth modules and keyfile so password file and encryption keyfile can be different

  • setup.py

     
    862862
    863863
    864864#*******************************************************************************
    865 toggle_packages(server_ENABLED, "xpra.server", "xpra.server.stats")
     865toggle_packages(server_ENABLED, "xpra.server", "xpra.server.stats", "xpra.server.auth")
    866866if WIN32 and not server_ENABLED:
    867867    #with py2exe, we have to remove the default packages and let it figure it out...
    868868    #(otherwise, we can't remove specific files from those packages)
  • xpra/client/client_base.py

     
    1818from xpra.scripts.config import ENCRYPTION_CIPHERS, python_platform
    1919from xpra.version_util import version_compat_check, add_version_info
    2020from xpra.platform.features import GOT_PASSWORD_PROMPT_SUGGESTION
    21 from xpra.os_util import get_hex_uuid, get_machine_id, SIGNAMES, strtobytes, bytestostr
     21from xpra.os_util import get_hex_uuid, get_machine_id, load_binary_file, SIGNAMES, strtobytes, bytestostr
    2222from xpra.util import typedict
    2323
    2424EXIT_OK = 0
     
    5454        self.password_file = None
    5555        self.password_sent = False
    5656        self.encryption = None
     57        self.encryption_keyfile = None
    5758        self.quality = -1
    5859        self.min_quality = 0
    5960        self.speed = 0
     
    8081        self.compression_level = opts.compression_level
    8182        self.password_file = opts.password_file
    8283        self.encryption = opts.encryption
     84        self.encryption_keyfile = opts.encryption_keyfile
    8385        self.quality = opts.quality
    8486        self.min_quality = opts.min_quality
    8587        self.speed = opts.speed
     
    165167            capabilities["cipher.key_salt"] = key_salt
    166168            iterations = 1000
    167169            capabilities["cipher.key_stretch_iterations"] = iterations
    168             self._protocol.set_cipher_in(self.encryption, iv, self.get_password(), key_salt, iterations)
     170            key = self.get_encryption_key()
     171            if key is None:
     172                self.warn_and_quit(EXIT_ENCRYPTION, "encryption key is missing")
     173                return
     174            self._protocol.set_cipher_in(self.encryption, iv, key, key_salt, iterations)
    169175            log("encryption capabilities: %s", [(k,v) for k,v in capabilities.items() if k.startswith("cipher")])
    170176        capabilities["platform"] = sys.platform
    171177        capabilities["platform.release"] = python_platform.release()
     
    175181        capabilities["namespace"] = True
    176182        capabilities["raw_packets"] = True
    177183        capabilities["chunked_compression"] = True
     184        capabilities["digest"] = "hmac", "xor"
    178185        capabilities["rencode"] = has_rencode
    179186        capabilities["lz4"] = has_lz4
    180187        if has_rencode:
     
    304311        if self.encryption:
    305312            assert len(packet)>=3, "challenge does not contain encryption details to use for the response"
    306313            server_cipher = packet[2]
    307             self.set_server_encryption(server_cipher)
    308         import hmac
    309         challenge_response = hmac.HMAC(self.password, salt)
    310         password_hash = challenge_response.hexdigest()
     314            key = self.get_encryption_key()
     315            if key is None:
     316                self.warn_and_quit(EXIT_ENCRYPTION, "encryption key is missing")
     317                return
     318            self.set_server_encryption(server_cipher, key)
     319        digest = "hmac"
     320        if len(packet)>=4:
     321            digest = packet[3]
     322        if digest=="hmac":
     323            import hmac
     324            challenge_response = hmac.HMAC(self.password, salt).hexdigest()
     325        elif digest=="xor":
     326            #don't send XORed password unencrypted:
     327            if not self._protocol.cipher_out:
     328                self.warn_and_quit(EXIT_ENCRYPTION, "server requested digest %s, cowardly refusing to use it without encryption" % digest)
     329                return
     330            from xpra.util import xor
     331            challenge_response = xor(self.password, salt)
     332        else:
     333            self.warn_and_quit(EXIT_PASSWORD_REQUIRED, "server requested an unsupported digest: %s" % digest)
     334            return
    311335        self.password_sent = True
    312         self.send_hello(password_hash)
     336        self.send_hello(challenge_response)
    313337
    314     def set_server_encryption(self, capabilities):
     338    def set_server_encryption(self, capabilities, key):
    315339        def get(key, default=None):
    316340            return capabilities.get(strtobytes(key), default)
    317341        cipher = get("cipher")
     
    324348        if cipher not in ENCRYPTION_CIPHERS:
    325349            self.warn_and_quit(EXIT_ENCRYPTION, "unsupported server cipher: %s, allowed ciphers: %s" % (cipher, ", ".join(ENCRYPTION_CIPHERS)))
    326350            return False
    327         self._protocol.set_cipher_out(cipher, cipher_iv, self.get_password(), key_salt, iterations)
     351        self._protocol.set_cipher_out(cipher, cipher_iv, key, key_salt, iterations)
    328352
    329353
    330     def get_password(self):
    331         if self.password is None:
    332             self.load_password()
    333         return self.password
     354    def get_encryption_key(self):
     355        key = load_binary_file(self.encryption_keyfile)
     356        if key is None and self.password_file:
     357            key = load_binary_file(self.password_file)
     358            if key:
     359                log("used password file as encryption key")
     360        if key is None:
     361            raise Exception("failed to load encryption keyfile %s" % self.encryption_keyfile)
     362        return key.strip("\n\r")
    334363
    335364    def load_password(self):
    336         try:
    337             filename = os.path.expanduser(self.password_file)
    338             passwordFile = open(filename, "rU")
    339             self.password = passwordFile.read()
    340             passwordFile.close()
    341             while self.password.endswith("\n") or self.password.endswith("\r"):
    342                 self.password = self.password[:-1]
    343         except IOError, e:
     365        filename = os.path.expanduser(self.password_file)
     366        self.password = load_binary_file(filename)
     367        if self.password is None:
    344368            self.warn_and_quit(EXIT_PASSWORD_FILE_ERROR, "failed to open password file %s: %s" % (self.password_file, e))
    345369            return False
     370        self.password = self.password.strip("\n\r")
    346371        log("password read from file %s is %s", self.password_file, self.password)
    347372        return True
    348373
     
    375400        self._protocol.chunked_compression = c.boolget("chunked_compression")
    376401        if use_rencode and c.boolget("rencode"):
    377402            self._protocol.enable_rencode()
    378         if c.boolget("lz4") and has_lz4 and self._protocol.chunked_compression and self.compression_level>0 and self.compression_level<3:
     403        if c.boolget("lz4") and has_lz4 and self._protocol.chunked_compression and self.compression_level==1:
    379404            self._protocol.enable_lz4()
    380405        if self.encryption:
    381406            #server uses a new cipher after second hello:
    382             self.set_server_encryption(c)
     407            key = self.get_encryption_key()
     408            assert key, "encryption key is missing"
     409            self.set_server_encryption(c, key)
    383410        self._protocol.aliases = c.dictget("aliases", {})
    384411        if self.pings:
    385412            self.timeout_add(1000, self.send_ping)
  • xpra/dotxpra.py

     
    1919    pass
    2020
    2121class DotXpra(object):
    22     def __init__(self, sockdir=None, confdir=None):
     22    def __init__(self, sockdir=None, confdir=None, actual_username=""):
    2323        from xpra.platform.paths import get_default_socket_dir, get_default_conf_dir
    2424        def expand(s):
     25            if len(actual_username)>0 and s.startswith("~/"):
     26                #replace "~/" with "~$actual_username/"
     27                s = "~%s/%s" % (actual_username, s[2:])
    2528            return os.path.expandvars(os.path.expanduser(s))
    2629        self._confdir = expand(confdir or get_default_conf_dir())
    2730        self._sockdir = expand(sockdir or get_default_socket_dir())
     
    103106            os.unlink(socket_path)
    104107        return socket_path
    105108
    106     def sockets(self):
     109    def sockets(self, check_uid=0):
    107110        results = []
    108111        base = os.path.join(self._sockdir, self._prefix)
    109112        potential_sockets = glob.glob(base + "*")
    110113        for path in potential_sockets:
    111             if stat.S_ISSOCK(os.stat(path).st_mode):
     114            s = os.stat(path)
     115            if stat.S_ISSOCK(s.st_mode):
     116                if check_uid>0:
     117                        if s.st_uid!=check_uid:
     118                                #socket uid does not match
     119                                continue
    112120                local_display = ":" + path[len(base):]
    113121                state = self.server_state(local_display)
    114122                results.append((state, local_display))
  • xpra/net/protocol.py

     
    628628                    debug("received %s encrypted bytes with %s padding", payload_size, len(padding))
    629629                    data = self.cipher_in.decrypt(raw_string)
    630630                    if padding:
    631                         def debug_str():
     631                        def debug_str(s):
    632632                            try:
    633                                 return list(bytearray(raw_string))
     633                                return list(bytearray(s))
    634634                            except:
    635                                 return list(str(raw_string))
    636                         assert data.endswith(padding), "decryption failed: string does not end with '%s': %s (%s) -> %s (%s)" % \
    637                             (padding, debug_str(raw_string), type(raw_string), debug_str(data), type(data))
     635                                return list(str(s))
     636                        if not data.endswith(padding):
     637                                log("decryption failed: string does not end with '%s': %s (%s) -> %s (%s)",
     638                            padding, debug_str(raw_string), type(raw_string), debug_str(data), type(data))
     639                                self._connection_lost("encryption error (wrong key?)")
     640                                return
    638641                        data = data[:-len(padding)]
    639642                #uncompress if needed:
    640643                if compression_level>0:
  • xpra/os_util.py

     
    157157                pass
    158158    return  str(v).strip("\n\r")
    159159
     160def load_binary_file(filename):
     161    if not os.path.exists(filename):
     162        return None
     163    f = None
     164    try:
     165        f = open(filename, "rU")
     166        try:
     167            return f.read()
     168        finally:
     169            f.close()
     170    except:
     171        return None
    160172
     173
    161174def main():
    162175    import logging
    163176    logging.basicConfig(format="%(asctime)s %(message)s")
  • xpra/scripts/config.py

     
    373373                    "title"             : str,
    374374                    "host"              : str,
    375375                    "username"          : str,
     376                    "auth"                              : str,
    376377                    "remote-xpra"       : str,
    377378                    "session-name"      : str,
    378379                    "client-toolkit"    : str,
     
    384385                    "clipboard-filter-file" : str,
    385386                    "pulseaudio-command": str,
    386387                    "encryption"        : str,
     388                    "encryption_keyfile": str,
    387389                    "mode"              : str,
    388390                    "ssh"               : str,
    389391                    "xvfb"              : str,
     
    452454                    "title"             : "@title@ on @client-machine@",
    453455                    "host"              : "",
    454456                    "username"          : username,
     457                    "auth"                              : "",
    455458                    "remote-xpra"       : ".xpra/run-xpra",
    456459                    "session-name"      : "",
    457460                    "client-toolkit"    : "",
     
    466469                                            +" --load=module-null-sink --load=module-native-protocol-unix "
    467470                                            +" --log-level=2 --log-target=stderr",
    468471                    "encryption"        : "",
     472                    "encryption_keyfile": "",
    469473                    "mode"              : "tcp",
    470474                    "ssh"               : DEFAULT_SSH_CMD,
    471475                    "xvfb"              : "Xvfb +extension Composite -screen 0 3840x2560x24+32 -nolisten tcp -noreset -auth $XAUTHORITY",
  • xpra/scripts/main.py

     
    301301    group.add_option("--ssh", action="store",
    302302                      dest="ssh", default=defaults.ssh, metavar="CMD",
    303303                      help="How to run ssh (default: '%default')")
     304    group.add_option("--username", action="store",
     305                      dest="username", default=defaults.username,
     306                      help="The username supplied by the client for authentication (default: '%default')")
     307    group.add_option("--auth", action="store",
     308                      dest="auth", default=defaults.auth,
     309                      help="The authentication module (default: '%default')")
    304310    group.add_option("--mmap-group", action="store_true",
    305311                      dest="mmap_group", default=defaults.mmap_group,
    306312                      help="When creating the mmap file with the client, set the group permission on the mmap file to the same value as the owner of the server socket file we connect to (default: '%default')")
     
    318324        group.add_option("--encryption", action="store",
    319325                          dest="encryption", default=defaults.encryption,
    320326                          metavar="ALGO",
    321                           help="Specifies the encryption cipher to use, only %s is currently supported. (default: None)" % (", ".join(ENCRYPTION_CIPHERS)))
     327                          help="Specifies the encryption cipher to use, supported algorithms are: %s (default: None)" % (", ".join(ENCRYPTION_CIPHERS)))
     328        group.add_option("--encryption-keyfile", action="store",
     329                          dest="encryption_keyfile", default=defaults.encryption_keyfile,
     330                          metavar="FILE",
     331                          help="Specifies the file containing the encryption key. (default: '%default')")
    322332    else:
    323333        hidden_options["encryption"] = ''
     334        hidden_options["encryption_keyfile"] = ''
    324335
    325336    options, args = parser.parse_args(cmdline[1:])
    326337    if not args:
     
    367378        assert len(ENCRYPTION_CIPHERS)>0, "cannot use encryption: no ciphers available"
    368379        if options.encryption not in ENCRYPTION_CIPHERS:
    369380            parser.error("encryption %s is not supported, try: %s" % (options.encryption, ", ".join(ENCRYPTION_CIPHERS)))
    370         if not options.password_file:
    371             parser.error("encryption %s cannot be used without a password (see --password-file option)" % options.encryption)
     381        if not options.password_file and not options.encryption_keyfile:
     382            parser.error("encryption %s cannot be used without a keyfile (see --encryption-keyfile option)" % options.encryption)
    372383    #ensure opengl is either True, False or None
    373384    options.opengl = parse_bool("opengl", options.opengl)
    374385
  • xpra/server/auth/__init__.py

     
     1#!/usr/bin/env python
     2# This file is part of Xpra.
     3# Copyright (C) 2013 Antoine Martin <antoine@devloop.org.uk>
     4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
     5# later version. See the file COPYING for details.
  • xpra/server/auth/file_auth.py

     
     1# This file is part of Xpra.
     2# Copyright (C) 2013 Antoine Martin <antoine@devloop.org.uk>
     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.path
     7import sys
     8import pwd
     9import hmac
     10
     11from xpra.os_util import get_hex_uuid
     12from xpra.dotxpra import DotXpra
     13from xpra.log import Logger, debug_if_env
     14log = Logger()
     15debug = debug_if_env(log, "XPRA_AUTH_DEBUG")
     16
     17
     18password_file = None
     19socket_dir = None
     20def init(opts):
     21    global password_file, socket_dir
     22    password_file = opts.password_file
     23    socket_dir = opts.socket_dir
     24
     25
     26auth_data = None
     27auth_data_time = None
     28def load_auth_file():
     29    global auth_data, auth_data_time, password_file, socket_dir
     30    if not os.path.exists(password_file):
     31        log.error("password file is missing: %s", e)
     32        auth_data = None
     33        return auth_data
     34    ptime = 0
     35    try:
     36        ptime = os.stat(password_file).st_mtime
     37    except Exception, e:
     38        log.error("error accessing password file time: %s", e)
     39    if auth_data is None or ptime!=auth_data_time:
     40        auth_data = {}
     41        auth_data_time = ptime
     42        f = None
     43        try:
     44            try:
     45                f = open(password_file, mode='rb')
     46                data = f.read()
     47            finally:
     48                if f:
     49                    f.close()
     50        except Exception, e:
     51            log.error("error loading %s: %s", password_file, e)
     52            data = ""
     53        i = 0
     54        for line in data.splitlines():
     55            i += 1
     56            line = line.strip()
     57            if len(line)==0:
     58                continue
     59            if line.find(":")<0:
     60                #assume old style file with just the password
     61                #get all the displays for the current user:
     62                sockdir = DotXpra(socket_dir)
     63                results = sockdir.sockets()
     64                displays = [display for state, display in results if state==DotXpra.LIVE]
     65                auth_data[""] = line, os.getuid(), displays, {}, {}
     66                continue
     67            ldata = line.split(":")
     68            if len(ldata)<4:
     69                log.warn("skipped line %s of %s: not enough fields", i, password_file)
     70                continue
     71            #parse fields:
     72            username = ldata[0]
     73            password = ldata[1]
     74            try:
     75                if ldata[2]=="":
     76                    uid = os.getuid()
     77                else:
     78                    uid = int(ldata[2])
     79            except Exception, e:
     80                log.error("invalid uid at line %s of %s: %s", i, password_file, e)
     81                continue
     82            displays = ldata[3].split(",")
     83            env_options = {}
     84            session_options = {}
     85            if len(ldata)>=5:
     86                env_options = parseOptions(ldata[4])
     87            if len(ldata)>=6:
     88                session_options = parseOptions(ldata[5])
     89            auth_data[username] = password, uid, displays, env_options, session_options
     90    return auth_data
     91
     92
     93class Authenticator(object):
     94    def __init__(self, username):
     95        self.username = username
     96        self.salt = None
     97        self.sessions = None
     98
     99    def get_challenge(self):
     100        if self.salt is not None:
     101            log.error("challenge already sent!")
     102            return None
     103        self.salt = get_hex_uuid()
     104        #this authenticator can use the safer "hmac" digest:
     105        return self.salt, "hmac"
     106
     107    def get_entry(self):
     108        ad = load_auth_file()
     109        username = self.username
     110        if username not in ad:
     111            #maybe this is an old style file with just the password?
     112            if len(ad)==1 and ad.keys()[0]=="":
     113                #then ignore the username
     114                username = ""
     115            else:
     116                return None
     117        return ad[username]
     118
     119    def get_password(self):
     120        entry = self.get_entry()
     121        if entry is None:
     122            return None
     123        return entry[0]
     124
     125    def authenticate(self, challenge_response):
     126        global password_file
     127        if not self.salt:
     128            log.error("illegal challenge response received - salt cleared or unset")
     129            return None
     130        #ensure this salt does not get re-used:
     131        salt = self.salt
     132        self.salt = None
     133        entry = self.get_entry()
     134        if entry is None:
     135            log.error("usename %s does not exist in %s", username, password_file)
     136            return None
     137        fpassword, uid, displays, env_options, session_options = entry
     138        hash = hmac.HMAC(fpassword, salt).hexdigest()
     139        log("authenticate(%s) password=%s, salt=%s, hash=%s", challenge_response, fpassword, salt, hash)
     140        if hash!=challenge_response:
     141            log.error("hmac password challenge for %s does not match", self.username)
     142            return False
     143        self.sessions = uid, displays, env_options, session_options
     144        return True
     145
     146    def get_sessions(self):
     147        return self.sessions
     148
     149    def __str__(self):
     150        return "Password File Authenticator"
  • xpra/server/auth/pam.py

    Property changes on: xpra/server/auth/file_auth.py
    ___________________________________________________________________
    Added: svn:executable
    ## -0,0 +1 ##
    +*
    \ No newline at end of property
     
     1#!/usr/bin/env python
     2# (c) 2007 Chris AtLee <chris@atlee.ca>
     3# Licensed under the MIT license:
     4# http://www.opensource.org/licenses/mit-license.php
     5"""
     6PAM module for python
     7
     8Provides an authenticate function that will allow the caller to authenticate
     9a user against the Pluggable Authentication Modules (PAM) on the system.
     10
     11Implemented using ctypes, so no compilation is necessary.
     12"""
     13__all__ = ['authenticate']
     14
     15from ctypes import CDLL, POINTER, Structure, CFUNCTYPE, cast, pointer, sizeof, cdll             #@UnresolvedImport
     16from ctypes import c_void_p, c_uint, c_char_p, c_char, c_int                                                    #@UnresolvedImport
     17from ctypes.util import find_library                                                                                                    #@UnresolvedImport
     18
     19paml = find_library("pam")
     20if paml:
     21    LIBPAM = CDLL(paml)
     22else:
     23    import sys
     24    if sys.platform.startswith("darwin"):
     25        LIBPAM = cdll.LoadLibrary("/usr/lib/libpam.dylib")
     26    else:
     27        #solaris doesn't find much on its own...
     28        LIBPAM = cdll.LoadLibrary("/lib/libpam.so.1")
     29LIBC = CDLL(find_library("c"))
     30
     31CALLOC = LIBC.calloc
     32CALLOC.restype = c_void_p
     33CALLOC.argtypes = [c_uint, c_uint]
     34
     35STRDUP = LIBC.strdup
     36STRDUP.argstypes = [c_char_p]
     37STRDUP.restype = POINTER(c_char) # NOT c_char_p !!!!
     38
     39# Various constants
     40PAM_PROMPT_ECHO_OFF = 1
     41PAM_PROMPT_ECHO_ON = 2
     42PAM_ERROR_MSG = 3
     43PAM_TEXT_INFO = 4
     44
     45class PamHandle(Structure):
     46    """wrapper class for pam_handle_t"""
     47    _fields_ = [
     48            ("handle", c_void_p)
     49            ]
     50
     51    def __init__(self):
     52        Structure.__init__(self)
     53        self.handle = 0
     54
     55class PamMessage(Structure):
     56    """wrapper class for pam_message structure"""
     57    _fields_ = [
     58            ("msg_style", c_int),
     59            ("msg", c_char_p),
     60            ]
     61
     62    def __repr__(self):
     63        return "<PamMessage %i '%s'>" % (self.msg_style, self.msg)
     64
     65class PamResponse(Structure):
     66    """wrapper class for pam_response structure"""
     67    _fields_ = [
     68            ("resp", c_char_p),
     69            ("resp_retcode", c_int),
     70            ]
     71
     72    def __repr__(self):
     73        return "<PamResponse %i '%s'>" % (self.resp_retcode, self.resp)
     74
     75CONV_FUNC = CFUNCTYPE(c_int,
     76        c_int, POINTER(POINTER(PamMessage)),
     77               POINTER(POINTER(PamResponse)), c_void_p)
     78
     79class PamConv(Structure):
     80    """wrapper class for pam_conv structure"""
     81    _fields_ = [
     82            ("conv", CONV_FUNC),
     83            ("appdata_ptr", c_void_p)
     84            ]
     85
     86PAM_START = LIBPAM.pam_start
     87PAM_START.restype = c_int
     88PAM_START.argtypes = [c_char_p, c_char_p, POINTER(PamConv),
     89        POINTER(PamHandle)]
     90
     91PAM_AUTHENTICATE = LIBPAM.pam_authenticate
     92PAM_AUTHENTICATE.restype = c_int
     93PAM_AUTHENTICATE.argtypes = [PamHandle, c_int]
     94
     95def authenticate(username, password, service='login'):
     96    """Returns True if the given username and password authenticate for the
     97    given service.  Returns False otherwise
     98
     99    ``username``: the username to authenticate
     100
     101    ``password``: the password in plain text
     102
     103    ``service``: the PAM service to authenticate against.
     104                 Defaults to 'login'"""
     105    @CONV_FUNC
     106    def my_conv(n_messages, messages, p_response, app_data):
     107        """Simple conversation function that responds to any
     108        prompt where the echo is off with the supplied password"""
     109        # Create an array of n_messages response objects
     110        addr = CALLOC(n_messages, sizeof(PamResponse))
     111        p_response[0] = cast(addr, POINTER(PamResponse))
     112        for i in range(n_messages):
     113            if messages[i].contents.msg_style == PAM_PROMPT_ECHO_OFF:
     114                pw_copy = STRDUP(str(password))
     115                p_response.contents[i].resp = cast(pw_copy, c_char_p)
     116                p_response.contents[i].resp_retcode = 0
     117        return 0
     118
     119    handle = PamHandle()
     120    conv = PamConv(my_conv, 0)
     121    retval = PAM_START(service, username, pointer(conv), pointer(handle))
     122
     123    if retval != 0:
     124        # TODO: This is not an authentication error, something
     125        # has gone wrong starting up PAM
     126        return False
     127
     128    retval = PAM_AUTHENTICATE(handle, 0)
     129    return retval == 0
     130
     131if __name__ == "__main__":
     132    import getpass
     133    print(authenticate(getpass.getuser(), getpass.getpass()))
  • xpra/server/auth/pam_auth.py

     
     1# This file is part of Xpra.
     2# Copyright (C) 2013 Antoine Martin <antoine@devloop.org.uk>
     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 sys
     7import pwd
     8
     9from xpra.dotxpra import DotXpra
     10from xpra.server.auth.sys_auth_base import SysAuthenticator, log, debug, init
     11
     12
     13check = None
     14#choice of two pam modules we can use
     15try:
     16    import PAM                        #@UnresolvedImport
     17    PAM_SERVICE = 'login'
     18    PAM_PASSWORD = "password"
     19
     20    class PAM_conv:
     21        def __init__(self, password):
     22            self.password = password
     23
     24        def pam_conv_password(auth, query_list, *args):
     25            try:
     26                resp = []
     27                for i in range(len(query_list)):
     28                    query, pam_type = query_list[i]
     29                    if pam_type == PAM.PAM_PROMPT_ECHO_ON or pam_type == PAM.PAM_PROMPT_ECHO_OFF:
     30                        resp.append((self.password, 0))
     31                    elif pam_type == PAM.PAM_PROMPT_ERROR_MSG or pam_type == PAM.PAM_PROMPT_TEXT_INFO:
     32                        log("pam_conf_password: ERROR/INFO: '%s'", query)
     33                        resp.append(('', 0))
     34                    else:
     35                        log.error("pam_conf_password unknown type: '%s'", pam_type)
     36            except Exception, e:
     37                log.error("pam_conv_password error: %s", e)
     38            return    resp
     39
     40    def check(username, password):
     41        debug("PAM check(%s, [..])", username)
     42        auth = PAM.pam()
     43        auth.start(PAM_SERVICE)
     44        auth.set_item(PAM.PAM_USER, username)
     45        conv = PAM_conv(password)
     46        auth.set_item(PAM.PAM_CONV, conv.pam_conv_password)
     47        try:
     48            auth.authenticate()
     49            return    True
     50            #auth.acct_mgmt()
     51        except PAM.error, resp:
     52            log.error("PAM.authenticate() error: %s", resp)
     53            return    False
     54        except Exception, e:
     55            log.error("PAM.authenticate() internal error: %s", e)
     56            return    False
     57except Exception, e:
     58    debug("PAM module not available: %s", e)
     59
     60try:
     61    from xpra.server.auth import pam
     62    assert pam
     63    def check(username, password):
     64        debug("pam check(%s, [..])", username)
     65        return pam.authenticate(username, password)
     66except:
     67    debug("pam module not available: %s", e)
     68
     69
     70if check is None:
     71    raise ImportError("cannot use pam_auth without a pam python module")
     72
     73
     74class Authenticator(SysAuthenticator):
     75
     76    def get_uid(self):
     77        #get the uid from the password database:
     78        pw = pwd.getpwnam(username)
     79        return pw.pw_uid
     80
     81    def check(self, password):
     82        return check(self.username, password)
     83
     84    def __str__(self):
     85        return "PAM Authenticator"
  • xpra/server/auth/sys_auth_base.py

    Property changes on: xpra/server/auth/pam_auth.py
    ___________________________________________________________________
    Added: svn:executable
    ## -0,0 +1 ##
    +*
    \ No newline at end of property
     
     1# This file is part of Xpra.
     2# Copyright (C) 2013 Antoine Martin <antoine@devloop.org.uk>
     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 sys
     7import pwd
     8
     9from xpra.dotxpra import DotXpra
     10from xpra.util import xor
     11from xpra.os_util import get_hex_uuid
     12from xpra.log import Logger, debug_if_env
     13log = Logger()
     14debug = debug_if_env(log, "XPRA_AUTH_DEBUG")
     15
     16
     17socket_dir = None
     18def init(opts):
     19    global socket_dir
     20    socket_dir = opts.socket_dir
     21
     22
     23class SysAuthenticator(object):
     24    def __init__(self, username):
     25        self.username = username
     26        self.salt = None
     27
     28    def get_challenge(self):
     29        if self.salt is not None:
     30            log.error("challenge already sent!")
     31            return None
     32        self.salt = get_hex_uuid()+get_hex_uuid()
     33        #we need the raw password, so tell the client to use "xor":
     34        return self.salt, "xor"
     35
     36    def get_uid(self):
     37        raise NotImplementedError()
     38
     39    def get_password(self):
     40        return None
     41
     42    def check(self, username, password):
     43        raise NotImplementedError()
     44
     45    def authenticate(self, challenge_response):
     46        global socket_dir
     47        if self.salt is None:
     48            log.error("got a challenge response with no salt!")
     49            return False
     50        password = xor(challenge_response, self.salt)
     51        #warning: enabling logging here would log the actual system password!
     52        #log("authenticate(%s) password=%s", challenge_response, password)
     53        #verify login:
     54        try :
     55            if not self.check(password):
     56                return False
     57        except Exception, e:
     58            log.error("authentication error: %s", e)
     59            return False
     60        return True
     61
     62    def get_sessions(self):
     63        sockdir = DotXpra(socket_dir, actual_username=self.username)
     64        uid = self.get_uid()
     65        results = sockdir.sockets(check_uid=uid)
     66        displays = [display for state, display in results if state==DotXpra.LIVE]
     67        return uid, displays, {}, {}
  • xpra/server/auth/win32_auth.py

    Property changes on: xpra/server/auth/sys_auth_base.py
    ___________________________________________________________________
    Added: svn:executable
    ## -0,0 +1 ##
    +*
    \ No newline at end of property
     
     1# This file is part of Xpra.
     2# Copyright (C) 2013 Antoine Martin <antoine@devloop.org.uk>
     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
     7
     8from xpra.server.auth.sys_auth_base import SysAuthenticator, init
     9import win32security            #@UnresolvedImport
     10assert win32security            #avoid pydev warning
     11
     12
     13class Authenticator(SysAuthenticator):
     14
     15    def get_uid(self):
     16        #uid is left unchanged:
     17        return os.getuid()
     18
     19    def check(self, password):
     20        win32security.LogonUser(username, '', password, win32security.LOGON32_LOGON_NETWORK, win32security.LOGON32_PROVIDER_DEFAULT)
     21
     22    def __str__(self):
     23        return "Win32 Authenticator"
  • xpra/server/auth_proxy.py

    Property changes on: xpra/server/auth/win32_auth.py
    ___________________________________________________________________
    Added: svn:executable
    ## -0,0 +1 ##
    +*
    \ No newline at end of property
     
    1 # coding=utf8
    21# This file is part of Xpra.
    32# Copyright (C) 2013 Antoine Martin <antoine@devloop.org.uk>
    43# Copyright (C) 2008 Nathaniel Smith <njs@pobox.com>
    54# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
    65# later version. See the file COPYING for details.
    76
     7import os
    88import gobject
    99gobject.threads_init()
    1010
     
    1919from xpra.os_util import Queue
    2020set_scheduler(gobject)
    2121
     22USE_THREADING = os.environ.get("XPRA_USE_THREADING", "0")=="1"
     23if USE_THREADING:
     24    #use threads
     25    from threading import Thread as Process
     26else:
     27    #use processes:
     28    from multiprocessing import Process
    2229
     30
    2331class ProxyServer(ServerCore):
    2432
    2533    def __init__(self):
     
    3442        self.timeout_add = gobject.timeout_add
    3543        self.source_remove = gobject.source_remove
    3644
     45    def init(self, opts):
     46        log("ProxyServer.init(%s)", opts)
     47        if not opts.auth:
     48            raise Exception("The proxy server requires an authentication mode")
     49        ServerCore.init(opts)
     50
    3751    def do_run(self):
    3852        self.main_loop = gobject.MainLoop()
    3953        self.main_loop.run()
  • xpra/server/server_base.py

     
    618618        info["server.python.full_version"] = sys.version
    619619        info["server.python.version"] = sys.version_info[:3]
    620620        info["session.name"] = self.session_name or ""
    621         info["features.password_file"] = self.password_file or ""
     621        info["features.authenticator"] = str((self.auth_class or str)(""))
    622622        info["features.randr"] = self.randr
    623623        info["features.cursors"] = self.cursors
    624624        info["features.bell"] = self.bell
  • xpra/server/server_core.py

     
    99import types
    1010import os.path
    1111import sys
    12 import hmac
    1312import time
    1413import socket
    1514import signal
     
    2120from xpra.scripts.config import ENCRYPTION_CIPHERS, python_platform
    2221from xpra.scripts.server import deadly_signal
    2322from xpra.net.bytestreams import SocketConnection
    24 from xpra.os_util import set_application_name, get_hex_uuid, SIGNAMES
     23from xpra.os_util import set_application_name, get_hex_uuid, load_binary_file, SIGNAMES
    2524from xpra.version_util import version_compat_check, add_version_info
    2625from xpra.net.protocol import Protocol, has_rencode, has_lz4, rencode_version, use_rencode
    2726from xpra.util import typedict
     
    3938    def __init__(self):
    4039        log("ServerCore.__init__()")
    4140        self.start_time = time.time()
     41        self.auth_class = None
    4242
    4343        self._upgrading = False
    4444        #networking bits:
     
    5050        self.session_name = "Xpra"
    5151
    5252        #Features:
     53        self.digest_modes = ("hmac", )
     54        self.encryption_keyfile = None
     55        self.password_file = None
    5356        self.compression_level = 1
    54         self.password_file = ""
    5557
    5658        self.init_packet_handlers()
    5759        self.init_aliases()
     
    7072        self.session_name = opts.session_name
    7173        set_application_name(self.session_name)
    7274
     75        self.encryption_keyfile = opts.encryption_keyfile
     76        self.password_file = opts.password_file
    7377        self.compression_level = opts.compression_level
    74         self.password_file = opts.password_file
    7578
     79        self.init_auth(opts)
     80
     81    def init_auth(self, opts):
     82        auth = opts.auth
     83        if not auth and opts.password_file:
     84            log.warn("no authentication module specified with 'password_file', using 'file' based authentication")
     85            auth = "file"
     86        if auth=="":
     87            return
     88        elif auth=="file":
     89            from xpra.server.auth import file_auth
     90            auth_module = file_auth
     91        elif auth=="pam":
     92            from xpra.server.auth import pam_auth
     93            auth_module = pam_auth
     94        elif auth=="win32":
     95            from xpra.server.auth import win32_auth
     96            auth_module = win32_auth
     97        else:
     98            raise Exception("invalid auth module: %s" % auth)
     99        try:
     100            auth_module.init(opts)
     101        except Exception, e:
     102            raise Exception("failed to initialize %s module: %s" % (auth_module, e))
     103        try:
     104            self.auth_class = getattr(auth_module, "Authenticator")
     105        except Exception, e:
     106            raise Exception("Authenticator class not found in %s" % auth_module)
     107
    76108    def init_sockets(self, sockets):
    77109        ### All right, we're ready to accept customers:
    78110        for socktype, sock in sockets:
     
    169201        log.info("New connection received: %s", sc)
    170202        protocol = Protocol(sc, self.process_packet)
    171203        protocol.large_packets.append("info-response")
    172         protocol.salt = None
    173204        protocol.set_compression_level(self.compression_level)
     205        protocol.authenticator = None
    174206        self._potential_protocols.append(protocol)
    175207        protocol.start()
    176208        self.timeout_add(10*1000, self.verify_connection_accepted, protocol)
     
    208240        self.disconnect_client(proto, "invalid packet format")
    209241
    210242
    211     def _send_password_challenge(self, proto, server_cipher):
    212         proto.salt = get_hex_uuid()
    213         log.info("Password required, sending challenge")
    214         proto.send_now(("challenge", proto.salt, server_cipher))
    215 
    216     def _verify_password(self, proto, client_hash, password):
    217         salt = proto.salt
    218         proto.salt = None
    219         if not salt:
    220             self.send_disconnect(proto, "illegal challenge response received - salt cleared or unset")
    221             return False
    222         password_hash = hmac.HMAC(password, salt)
    223         if client_hash != password_hash.hexdigest():
    224             def login_failed(*args):
    225                 log.error("Password supplied does not match! dropping the connection.")
    226                 self.send_disconnect(proto, "invalid password")
    227             self.timeout_add(1000, login_failed)
    228             return False
    229         log.info("Password matches!")
    230         sys.stdout.flush()
    231         return True
    232 
    233     def get_password(self):
    234         if not self.password_file:
    235             return None
    236         filename = os.path.expanduser(self.password_file)
    237         if not filename:
    238             return None
    239         try:
    240             passwordFile = open(filename, "rU")
    241             password  = passwordFile.read()
    242             passwordFile.close()
    243             while len(password)>0 and password[-1] in ("\n", "\r"):
    244                 password = password[:-1]
    245             return password
    246         except IOError, e:
    247             log.error("cannot open password file %s: %s", filename, e)
    248             return None
    249 
    250 
    251243    def _process_hello(self, proto, packet):
    252244        capabilities = packet[1]
    253245        c = typedict(capabilities)
     
    255247        proto.chunked_compression = c.boolget("chunked_compression")
    256248        if use_rencode and c.boolget("rencode"):
    257249            proto.enable_rencode()
    258         if c.boolget("lz4") and has_lz4 and proto.chunked_compression and self.compression_level>0 and self.compression_level<3:
     250        if c.boolget("lz4") and has_lz4 and proto.chunked_compression and self.compression_level==1:
    259251            proto.enable_lz4()
    260252
    261253        log("process_hello: capabilities=%s", capabilities)
     
    278270            self.disconnect_client(proto, "incompatible version: %s" % verr)
    279271            proto.close()
    280272            return  False
     273
     274        #authenticator:
     275        username = c.strget("username")
     276        if proto.authenticator is None and self.auth_class:
     277            proto.authenticator = self.auth_class(username)
     278        self.digest_modes = c.get("digest", ("hmac", ))
     279
    281280        #client may have requested encryption:
    282281        cipher = c.strget("cipher")
    283282        cipher_iv = c.strget("cipher.iv")
    284283        key_salt = c.strget("cipher.key_salt")
    285284        iterations = c.intget("cipher.key_stretch_iterations")
    286         password = None
    287         if bool(self.password_file) or (cipher is not None and cipher_iv is not None):
    288             #we will need the password:
    289             log("process_hello password is required!")
    290             password = self.get_password()
    291             if not password:
    292                 self.send_disconnect(proto, "password not found")
    293                 return False
    294285        auth_caps = {}
    295286        if cipher and cipher_iv:
    296287            if cipher not in ENCRYPTION_CIPHERS:
    297288                log.warn("unsupported cipher: %s", cipher)
    298289                self.send_disconnect(proto, "unsupported cipher")
    299290                return False
    300             proto.set_cipher_out(cipher, cipher_iv, password, key_salt, iterations)
     291            encryption_key = self.get_encryption_key(proto.authenticator)
     292            if encryption_key is None:
     293                self.send_disconnect(proto, "encryption key is missing")
     294                return
     295            proto.set_cipher_out(cipher, cipher_iv, encryption_key, key_salt, iterations)
    301296            #use the same cipher as used by the client:
    302297            iv = get_hex_uuid()[:16]
    303298            key_salt = get_hex_uuid()
    304299            iterations = 1000
    305             proto.set_cipher_in(cipher, iv, password, key_salt, iterations)
     300            proto.set_cipher_in(cipher, iv, encryption_key, key_salt, iterations)
    306301            auth_caps = {
    307302                         "cipher"           : cipher,
    308303                         "cipher.iv"        : iv,
     
    310305                         "cipher.key_stretch_iterations" : iterations
    311306                         }
    312307            log("server cipher=%s", auth_caps)
     308        else:
     309            auth_caps = None
    313310
    314         if self.password_file:
    315             log("password auth required")
     311        #verify authentication if required:
     312        if proto.authenticator:
     313            log("processing authentication with %s", proto.authenticator)
    316314            #send challenge if this is not a response:
    317             client_hash = c.strget("challenge_response")
    318             if not client_hash or not proto.salt:
    319                 self._send_password_challenge(proto, auth_caps or "")
     315            challenge_response = c.strget("challenge_response")
     316            if not challenge_response:
     317                challenge = proto.authenticator.get_challenge()
     318                if challenge is None:
     319                    self.timeout_add(1000, self.send_disconnect, proto, "invalid authentication state: unexpected challenge response")
     320                    return False
     321                log.info("Authentication required, sending challenge")
     322                salt, digest = challenge
     323                if digest not in self.digest_modes:
     324                    self.send_disconnect(proto, "cannot proceed without %s digest support" % digest)
     325                    return False
     326                proto.send_now(("challenge", salt, auth_caps or "", digest))
    320327                return False
    321             if not self._verify_password(proto, client_hash, password):
     328            if not proto.authenticator.authenticate(challenge_response):
     329                self.timeout_add(1000, self.send_disconnect, proto, "authentication failed")
    322330                return False
     331            log("authentication challenge passed")
    323332        return auth_caps
    324333
     334    def get_encryption_key(self, authenticator=None):
     335        #if we have a keyfile specified, use that:
     336        v = None
     337        if self.encryption_keyfile:
     338            log("trying to load encryption key from keyfile: %s", self.encryption_keyfile)
     339            v = load_binary_file(self.encryption_keyfile)
     340        if v is None and authenticator:
     341            log("trying to get encryption key from: %s", authenticator)
     342            v = authenticator.get_password()
     343        if v is None and self.password_file:
     344            log("trying to load encryption key from password file: %s", self.password_file)
     345            v = load_binary_file(self.password_file)
     346        if v is None:
     347                return None
     348        return v.strip("\n\r")
     349
    325350    def hello_oked(self, proto, packet, c, auth_caps):
    326351        pass
    327352
     
    350375        capabilities["elapsed_time"] = int(now - self.start_time)
    351376        capabilities["raw_packets"] = True
    352377        capabilities["chunked_compression"] = True
     378        capabilities["digest"] = ("hmac", "xor")
    353379        capabilities["lz4"] = has_lz4
    354380        capabilities["rencode"] = has_rencode
    355381        if has_rencode:
  • xpra/util.py

     
    7272    if x is None:
    7373        return None
    7474    return str(x).replace("\n", "\\n").replace("\r", "\\r")
     75
     76def xor(s1,s2):   
     77    return ''.join(chr(ord(a) ^ ord(b)) for a,b in zip(s1,s2))