pam.py 7.4 KB

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