20 | 20 |
'''
|
21 | 21 |
|
22 | 22 |
__all__ = ['pam']
|
23 | |
__version__ = '1.8.4'
|
|
23 |
__version__ = '1.8.5rc1'
|
24 | 24 |
__author__ = 'David Ford <david@blue-labs.org>'
|
25 | |
__released__ = '2018 June 15'
|
|
25 |
__released__ = '2019 November 12'
|
26 | 26 |
|
|
27 |
import os
|
27 | 28 |
import sys
|
28 | 29 |
|
29 | |
from ctypes import CDLL, POINTER, Structure, CFUNCTYPE, cast, byref, sizeof
|
30 | |
from ctypes import c_void_p, c_size_t, c_char_p, c_char, c_int
|
31 | |
from ctypes import memmove
|
32 | |
from ctypes.util import find_library
|
|
30 |
import PAM
|
33 | 31 |
|
34 | |
class PamHandle(Structure):
|
35 | |
"""wrapper class for pam_handle_t pointer"""
|
36 | |
_fields_ = [ ("handle", c_void_p) ]
|
37 | |
|
38 | |
def __init__(self):
|
39 | |
Structure.__init__(self)
|
40 | |
self.handle = 0
|
41 | |
|
42 | |
class PamMessage(Structure):
|
43 | |
"""wrapper class for pam_message structure"""
|
44 | |
_fields_ = [ ("msg_style", c_int), ("msg", c_char_p) ]
|
45 | |
|
46 | |
def __repr__(self):
|
47 | |
return "<PamMessage %i '%s'>" % (self.msg_style, self.msg)
|
48 | |
|
49 | |
class PamResponse(Structure):
|
50 | |
"""wrapper class for pam_response structure"""
|
51 | |
_fields_ = [ ("resp", c_char_p), ("resp_retcode", c_int) ]
|
52 | |
|
53 | |
def __repr__(self):
|
54 | |
return "<PamResponse %i '%s'>" % (self.resp_retcode, self.resp)
|
55 | |
|
56 | |
conv_func = CFUNCTYPE(c_int, c_int, POINTER(POINTER(PamMessage)), POINTER(POINTER(PamResponse)), c_void_p)
|
57 | |
|
58 | |
class PamConv(Structure):
|
59 | |
"""wrapper class for pam_conv structure"""
|
60 | |
_fields_ = [ ("conv", conv_func), ("appdata_ptr", c_void_p) ]
|
61 | |
|
62 | |
# Various constants
|
63 | |
PAM_PROMPT_ECHO_OFF = 1
|
64 | |
PAM_PROMPT_ECHO_ON = 2
|
65 | |
PAM_ERROR_MSG = 3
|
66 | |
PAM_TEXT_INFO = 4
|
67 | |
PAM_REINITIALIZE_CRED = 8
|
68 | |
|
69 | |
libc = CDLL(find_library("c"))
|
70 | |
libpam = CDLL(find_library("pam"))
|
71 | |
|
72 | |
calloc = libc.calloc
|
73 | |
calloc.restype = c_void_p
|
74 | |
calloc.argtypes = [c_size_t, c_size_t]
|
75 | |
|
76 | |
# bug #6 (@NIPE-SYSTEMS), some libpam versions don't include this function
|
77 | |
if hasattr(libpam, 'pam_end'):
|
78 | |
pam_end = libpam.pam_end
|
79 | |
pam_end.restype = c_int
|
80 | |
pam_end.argtypes = [PamHandle, c_int]
|
81 | |
|
82 | |
pam_start = libpam.pam_start
|
83 | |
pam_start.restype = c_int
|
84 | |
pam_start.argtypes = [c_char_p, c_char_p, POINTER(PamConv), POINTER(PamHandle)]
|
85 | |
|
86 | |
pam_setcred = libpam.pam_setcred
|
87 | |
pam_setcred.restype = c_int
|
88 | |
pam_setcred.argtypes = [PamHandle, c_int]
|
89 | |
|
90 | |
pam_strerror = libpam.pam_strerror
|
91 | |
pam_strerror.restype = c_char_p
|
92 | |
pam_strerror.argtypes = [PamHandle, c_int]
|
93 | |
|
94 | |
pam_authenticate = libpam.pam_authenticate
|
95 | |
pam_authenticate.restype = c_int
|
96 | |
pam_authenticate.argtypes = [PamHandle, c_int]
|
97 | 32 |
|
98 | 33 |
class pam():
|
99 | 34 |
code = 0
|
|
124 | 59 |
failure: False
|
125 | 60 |
"""
|
126 | 61 |
|
127 | |
@conv_func
|
128 | |
def my_conv(n_messages, messages, p_response, app_data):
|
129 | |
"""Simple conversation function that responds to any
|
130 | |
prompt where the echo is off with the supplied password"""
|
131 | |
# Create an array of n_messages response objects
|
132 | |
addr = calloc(n_messages, sizeof(PamResponse))
|
133 | |
response = cast(addr, POINTER(PamResponse))
|
134 | |
p_response[0] = response
|
135 | |
for i in range(n_messages):
|
136 | |
if messages[i].contents.msg_style == PAM_PROMPT_ECHO_OFF:
|
137 | |
dst = calloc(len(password)+1, sizeof(c_char))
|
138 | |
memmove(dst, cpassword, len(password))
|
139 | |
response[i].resp = dst
|
140 | |
response[i].resp_retcode = 0
|
141 | |
return 0
|
142 | |
|
143 | 62 |
# python3 ctypes prefers bytes
|
144 | 63 |
if sys.version_info >= (3,):
|
145 | 64 |
if isinstance(username, str): username = username.encode(encoding)
|
|
153 | 72 |
if isinstance(service, unicode):
|
154 | 73 |
service = service.encode(encoding)
|
155 | 74 |
|
156 | |
if b'\x00' in username or b'\x00' in password or b'\x00' in service:
|
157 | |
self.code = 4 # PAM_SYSTEM_ERR in Linux-PAM
|
158 | |
self.reason = 'strings may not contain NUL'
|
159 | |
return False
|
|
75 |
def conv(pam_self, query_list, user_data):
|
|
76 |
response = []
|
|
77 |
for prompt, msg in query_list:
|
|
78 |
if msg == PAM.PAM_PROMPT_ECHO_OFF:
|
|
79 |
response.append((password, PAM.PAM_SUCCESS))
|
|
80 |
else:
|
|
81 |
response.append((b'', PAM.PAM_SUCCESS))
|
|
82 |
return response
|
160 | 83 |
|
161 | |
# do this up front so we can safely throw an exception if there's
|
162 | |
# anything wrong with it
|
163 | |
cpassword = c_char_p(password)
|
|
84 |
# if X DISPLAY is set, use it, otherwise get the STDIN tty
|
|
85 |
ctty = os.environ.get('DISPLAY', os.ttyname(0)).encode(encoding)
|
164 | 86 |
|
165 | |
handle = PamHandle()
|
166 | |
conv = PamConv(my_conv, 0)
|
167 | |
retval = pam_start(service, username, byref(conv), byref(handle))
|
168 | |
|
169 | |
if retval != 0:
|
|
87 |
p = PAM.pam()
|
|
88 |
try:
|
|
89 |
p.start(service, username, conv)
|
|
90 |
except PAM.error as exc:
|
170 | 91 |
# This is not an authentication error, something has gone wrong starting up PAM
|
171 | |
self.code = retval
|
|
92 |
self.code = exc.errno
|
172 | 93 |
self.reason = "pam_start() failed"
|
173 | 94 |
return False
|
174 | 95 |
|
175 | |
retval = pam_authenticate(handle, 0)
|
176 | |
auth_success = retval == 0
|
177 | |
|
178 | |
if auth_success and resetcreds:
|
179 | |
retval = pam_setcred(handle, PAM_REINITIALIZE_CRED);
|
180 | |
|
181 | |
# store information to inform the caller why we failed
|
182 | |
self.code = retval
|
183 | |
self.reason = pam_strerror(handle, retval)
|
|
96 |
# set the TTY, needed when pam_securetty is used and the username root is used
|
|
97 |
p.set_item(PAM.PAM_TTY, ctty)
|
|
98 |
p.set_item(PAM.PAM_XDISPLAY, ctty)
|
|
99 |
try:
|
|
100 |
p.authenticate()
|
|
101 |
p.acct_mgmt()
|
|
102 |
if resetcreds:
|
|
103 |
p.setcred(PAM.PAM_REINITIALIZE_CRED)
|
|
104 |
except PAM.error as exc:
|
|
105 |
self.code = exc.errno
|
|
106 |
self.reason = exc.args[0]
|
|
107 |
else:
|
|
108 |
self.code = PAM.PAM_SUCCESS
|
|
109 |
self.reason = b'Success'
|
|
110 |
finally:
|
|
111 |
p.end()
|
184 | 112 |
if sys.version_info >= (3,):
|
185 | 113 |
self.reason = self.reason.decode(encoding)
|
186 | |
|
187 | |
if hasattr(libpam, 'pam_end'):
|
188 | |
pam_end(handle, retval)
|
189 | |
|
190 | |
return auth_success
|
|
114 |
return self.code == PAM.PAM_SUCCESS
|
191 | 115 |
|
192 | 116 |
|
193 | 117 |
def authenticate(*vargs, **dargs):
|