1# (c) 2007 Chris AtLee <chris@atlee.ca> 2# Licensed under the MIT license: 3# http://www.opensource.org/licenses/mit-license.php 4# 5# Original author: Chris AtLee 6# 7# Modified by David Ford, 2011-12-6 8# added py3 support and encoding 9# added pam_end 10# added pam_setcred to reset credentials after seeing Leon Walker's remarks 11# added byref as well 12# use readline to prestuff the getuser input 13 14''' 15PAM module for python 16 17Provides an authenticate function that will allow the caller to authenticate 18a user against the Pluggable Authentication Modules (PAM) on the system. 19 20Implemented using ctypes, so no compilation is necessary. 21''' 22 23__all__ = ['pam'] 24__version__ = '1.8.4' 25__author__ = 'David Ford <david@blue-labs.org>' 26__released__ = '2018 June 15' 27 28import sys 29 30from ctypes import CDLL, POINTER, Structure, CFUNCTYPE, cast, byref, sizeof 31from ctypes import c_void_p, c_size_t, c_char_p, c_char, c_int 32from ctypes import memmove 33from ctypes.util import find_library 34 35class PamHandle(Structure): 36 """wrapper class for pam_handle_t pointer""" 37 _fields_ = [ ("handle", c_void_p) ] 38 39 def __init__(self): 40 Structure.__init__(self) 41 self.handle = 0 42 43class PamMessage(Structure): 44 """wrapper class for pam_message structure""" 45 _fields_ = [ ("msg_style", c_int), ("msg", c_char_p) ] 46 47 def __repr__(self): 48 return "<PamMessage %i '%s'>" % (self.msg_style, self.msg) 49 50class PamResponse(Structure): 51 """wrapper class for pam_response structure""" 52 _fields_ = [ ("resp", c_char_p), ("resp_retcode", c_int) ] 53 54 def __repr__(self): 55 return "<PamResponse %i '%s'>" % (self.resp_retcode, self.resp) 56 57conv_func = CFUNCTYPE(c_int, c_int, POINTER(POINTER(PamMessage)), POINTER(POINTER(PamResponse)), c_void_p) 58 59class PamConv(Structure): 60 """wrapper class for pam_conv structure""" 61 _fields_ = [ ("conv", conv_func), ("appdata_ptr", c_void_p) ] 62 63# Various constants 64PAM_PROMPT_ECHO_OFF = 1 65PAM_PROMPT_ECHO_ON = 2 66PAM_ERROR_MSG = 3 67PAM_TEXT_INFO = 4 68PAM_REINITIALIZE_CRED = 8 69 70libc = CDLL(find_library("c")) 71libpam = CDLL(find_library("pam")) 72 73calloc = libc.calloc 74calloc.restype = c_void_p 75calloc.argtypes = [c_size_t, c_size_t] 76 77# bug #6 (@NIPE-SYSTEMS), some libpam versions don't include this function 78if hasattr(libpam, 'pam_end'): 79 pam_end = libpam.pam_end 80 pam_end.restype = c_int 81 pam_end.argtypes = [PamHandle, c_int] 82 83pam_start = libpam.pam_start 84pam_start.restype = c_int 85pam_start.argtypes = [c_char_p, c_char_p, POINTER(PamConv), POINTER(PamHandle)] 86 87pam_setcred = libpam.pam_setcred 88pam_setcred.restype = c_int 89pam_setcred.argtypes = [PamHandle, c_int] 90 91pam_strerror = libpam.pam_strerror 92pam_strerror.restype = c_char_p 93pam_strerror.argtypes = [PamHandle, c_int] 94 95pam_authenticate = libpam.pam_authenticate 96pam_authenticate.restype = c_int 97pam_authenticate.argtypes = [PamHandle, c_int] 98 99class pam(): 100 code = 0 101 reason = None 102 103 def __init__(self): 104 pass 105 106 def authenticate(self, username, password, service='login', encoding='utf-8', resetcreds=True): 107 """username and password authentication for the given service. 108 109 Returns True for success, or False for failure. 110 111 self.code (integer) and self.reason (string) are always stored and may 112 be referenced for the reason why authentication failed. 0/'Success' will 113 be stored for success. 114 115 Python3 expects bytes() for ctypes inputs. This function will make 116 necessary conversions using the supplied encoding. 117 118 Inputs: 119 username: username to authenticate 120 password: password in plain text 121 service: PAM service to authenticate against, defaults to 'login' 122 123 Returns: 124 success: True 125 failure: False 126 """ 127 128 @conv_func 129 def my_conv(n_messages, messages, p_response, app_data): 130 """Simple conversation function that responds to any 131 prompt where the echo is off with the supplied password""" 132 # Create an array of n_messages response objects 133 addr = calloc(n_messages, sizeof(PamResponse)) 134 response = cast(addr, POINTER(PamResponse)) 135 p_response[0] = response 136 for i in range(n_messages): 137 if messages[i].contents.msg_style == PAM_PROMPT_ECHO_OFF: 138 dst = calloc(len(password)+1, sizeof(c_char)) 139 memmove(dst, cpassword, len(password)) 140 response[i].resp = dst 141 response[i].resp_retcode = 0 142 return 0 143 144 # python3 ctypes prefers bytes 145 if sys.version_info >= (3,): 146 if isinstance(username, str): username = username.encode(encoding) 147 if isinstance(password, str): password = password.encode(encoding) 148 if isinstance(service, str): service = service.encode(encoding) 149 else: 150 if isinstance(username, unicode): 151 username = username.encode(encoding) 152 if isinstance(password, unicode): 153 password = password.encode(encoding) 154 if isinstance(service, unicode): 155 service = service.encode(encoding) 156 157 if b'\x00' in username or b'\x00' in password or b'\x00' in service: 158 self.code = 4 # PAM_SYSTEM_ERR in Linux-PAM 159 self.reason = 'strings may not contain NUL' 160 return False 161 162 # do this up front so we can safely throw an exception if there's 163 # anything wrong with it 164 cpassword = c_char_p(password) 165 166 handle = PamHandle() 167 conv = PamConv(my_conv, 0) 168 retval = pam_start(service, username, byref(conv), byref(handle)) 169 170 if retval != 0: 171 # This is not an authentication error, something has gone wrong starting up PAM 172 self.code = retval 173 self.reason = "pam_start() failed" 174 return False 175 176 retval = pam_authenticate(handle, 0) 177 auth_success = retval == 0 178 179 if auth_success and resetcreds: 180 retval = pam_setcred(handle, PAM_REINITIALIZE_CRED); 181 182 # store information to inform the caller why we failed 183 self.code = retval 184 self.reason = pam_strerror(handle, retval) 185 if sys.version_info >= (3,): 186 self.reason = self.reason.decode(encoding) 187 188 if hasattr(libpam, 'pam_end'): 189 pam_end(handle, retval) 190 191 return auth_success 192 193 194def authenticate(*vargs, **dargs): 195 """ 196 Compatibility function for older versions of python-pam. 197 """ 198 return pam().authenticate(*vargs, **dargs) 199 200 201if __name__ == "__main__": 202 import readline, getpass 203 204 def input_with_prefill(prompt, text): 205 def hook(): 206 readline.insert_text(text) 207 readline.redisplay() 208 readline.set_pre_input_hook(hook) 209 210 if sys.version_info >= (3,): 211 result = input(prompt) 212 else: 213 result = raw_input(prompt) 214 215 readline.set_pre_input_hook() 216 return result 217 218 pam = pam() 219 220 username = input_with_prefill('Username: ', getpass.getuser()) 221 222 # enter a valid username and an invalid/valid password, to verify both failure and success 223 pam.authenticate(username, getpass.getpass()) 224 print('{} {}'.format(pam.code, pam.reason)) 225