Hardware authentication for Linux using ordinary USB Flash Drives.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

319 lines
7.6 KiB

17 years ago
8 years ago
8 years ago
17 years ago
8 years ago
17 years ago
8 years ago
17 years ago
8 years ago
17 years ago
8 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
17 years ago
8 years ago
17 years ago
17 years ago
  1. #!/usr/bin/env python
  2. #
  3. # Copyright (c) 2003-2007 Andrea Luzzardi <scox@sig11.org>
  4. #
  5. # This file is part of the pam_usb project. pam_usb is free software;
  6. # you can redistribute it and/or modify it under the terms of the GNU General
  7. # Public License version 2, as published by the Free Software Foundation.
  8. #
  9. # pam_usb is distributed in the hope that it will be useful, but WITHOUT ANY
  10. # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
  12. # details.
  13. #
  14. # You should have received a copy of the GNU General Public License along with
  15. # this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
  16. # Street, Fifth Floor, Boston, MA 02110-1301 USA.
  17. import fcntl
  18. import getopt
  19. import gi
  20. import os
  21. import pwd
  22. import re
  23. import signal
  24. import subprocess
  25. import sys
  26. import syslog
  27. import threading
  28. gi.require_version('UDisks', '2.0')
  29. from gi.repository import GLib, UDisks
  30. import xml.etree.ElementTree as et
  31. class HotPlugDevice:
  32. def __init__(self, serial):
  33. self.__udi = None
  34. self.__serial = serial
  35. self.__callbacks = []
  36. self.__running = False
  37. def run(self):
  38. self.__scanDevices()
  39. self.__registerSignals()
  40. self.__running = True
  41. GLib.MainLoop().run()
  42. print('signals registered')
  43. def addCallback(self, callback):
  44. self.__callbacks.append(callback)
  45. def __scanDevices(self):
  46. for udi in udisksObjectManager.get_objects():
  47. if udi.get_block():
  48. device = udisks.get_drive_for_block(udi.get_block())
  49. if device:
  50. self.__deviceAdded(device)
  51. def __registerSignals(self):
  52. for signal, callback in (('object-added', self.__objectAdded),
  53. ('object-removed', self.__objectRemoved)):
  54. udisksObjectManager.connect(signal, callback)
  55. def __objectAdded(self, _, udi):
  56. if udi.get_block():
  57. device = udisks.get_drive_for_block(udi.get_block())
  58. if device:
  59. self.__deviceAdded(device)
  60. def __objectRemoved(self, _, udi):
  61. if udi.get_block():
  62. device = udisks.get_drive_for_block(udi.get_block())
  63. if device:
  64. self.__deviceRemoved(device)
  65. def __deviceAdded(self, udi):
  66. if self.__udi is not None:
  67. return
  68. if udi.get_property('serial') != self.__serial:
  69. return
  70. self.__udi = udi
  71. if self.__running:
  72. [ cb('added') for cb in self.__callbacks ]
  73. def __deviceRemoved(self, udi):
  74. if self.__udi is None:
  75. return
  76. if self.__udi != udi:
  77. return
  78. self.__udi = None
  79. if self.__running:
  80. [ cb('removed') for cb in self.__callbacks ]
  81. class Log:
  82. def __init__(self):
  83. syslog.openlog('pamusb-agent', syslog.LOG_PID | syslog.LOG_PERROR,
  84. syslog.LOG_AUTH)
  85. def info(self, message):
  86. self.__logMessage(syslog.LOG_NOTICE, message)
  87. def error(self, message):
  88. self.__logMessage(syslog.LOG_ERR, message)
  89. def __logMessage(self, priority, message):
  90. syslog.syslog(priority, message)
  91. def usage():
  92. print('Usage: %s [--help] [--config=path] [--daemon] [--check=path]' % \
  93. os.path.basename(__file__))
  94. sys.exit(1)
  95. def runAs(uid, gid):
  96. def set_id():
  97. os.setgid(gid)
  98. os.setuid(uid)
  99. return set_id
  100. import getopt
  101. try:
  102. opts, args = getopt.getopt(sys.argv[1:], "hc:dc:",
  103. ["help", "config=", "daemon", "check="])
  104. except getopt.GetoptError:
  105. usage()
  106. options = {'configFile' : '/etc/security/pam_usb.conf',
  107. 'daemon' : False,
  108. 'check' : '/usr/bin/pamusb-check'}
  109. if len(args) != 0:
  110. usage()
  111. for o, a in opts:
  112. if o in ('-h', '--help'):
  113. usage()
  114. if o in ('-c', '--config'):
  115. options['configFile'] = a
  116. if o in ('-d', '--daemon'):
  117. options['daemon'] = True
  118. if o in ('-c', '--check'):
  119. options['check'] = a
  120. if not os.path.exists(options['check']):
  121. print('%s not found.' % options['check'])
  122. print("You might specify manually pamusb-check's location using --check.")
  123. usage()
  124. logger = Log()
  125. doc = et.parse(options['configFile'])
  126. users = doc.findall('users/user')
  127. def userDeviceThread(user):
  128. userName = user.get('id')
  129. uid = pwd.getpwnam(userName)[2]
  130. gid = pwd.getpwnam(userName)[3]
  131. os.environ = None
  132. events = {
  133. 'lock' : [],
  134. 'unlock' : []
  135. }
  136. for hotplug in user.findall('agent'):
  137. henvs = {}
  138. hcmds = []
  139. for hcmd in hotplug.findall('cmd'):
  140. if hcmd.text is not None:
  141. hcmds.append(hcmd.text)
  142. else:
  143. logger.error('Ignoring empty command for user "%s".' % userName)
  144. for henv in hotplug.findall('env'):
  145. if henv.text is not None:
  146. henv_var = re.sub(r'^(.*?)=.*$', '\\1', henv.text)
  147. henv_arg = re.sub(r'^.*?=(.*)$', '\\1', henv.text)
  148. if henv_var != '' and henv_arg != '':
  149. henvs[henv_var] = henv_arg
  150. else:
  151. logger.error('Ignoring invalid command environment variable for user "%s".' % userName)
  152. else:
  153. logger.error('Ignoring empty environment variable for user "%s".' % userName)
  154. events[hotplug.get('event')].append(
  155. {
  156. 'env': henvs,
  157. 'cmd': hcmds
  158. }
  159. )
  160. deviceName = user.find('device').text.strip()
  161. devices = doc.findall("devices/device")
  162. deviceOK = False
  163. for device in devices:
  164. if device.get('id') == deviceName:
  165. deviceOK = True
  166. break
  167. if not deviceOK:
  168. logger.error('Device %s not found in configuration file.' % deviceName)
  169. return 1
  170. serial = device.find('serial').text.strip()
  171. def authChangeCallback(event):
  172. if event == 'removed':
  173. logger.info('Device "%s" has been removed, ' \
  174. 'locking down user "%s"...' % (deviceName, userName))
  175. for l in events['lock']:
  176. if len(l['cmd']) != 0:
  177. for cmd in l['cmd']:
  178. logger.info('Running "%s"' % cmd)
  179. subprocess.run(cmd.split(), env=l['env'], preexec_fn=runAs(uid, gid))
  180. logger.info('Locked.')
  181. return
  182. logger.info('Device "%s" has been inserted. ' \
  183. 'Performing verification...' % deviceName)
  184. cmdLine = "%s --debug --config=%s --service=pamusb-agent %s" % (
  185. options['check'], options['configFile'], userName)
  186. logger.info('Executing "%s"' % cmdLine)
  187. if not os.system(cmdLine):
  188. logger.info('Authentication succeeded. ' \
  189. 'Unlocking user "%s"...' % userName)
  190. for l in events['unlock']:
  191. if len(l['cmd']) != 0:
  192. for cmd in l['cmd']:
  193. logger.info('Running "%s"' % cmd)
  194. subprocess.run(cmd.split(), env=l['env'], preexec_fn=runAs(uid, gid))
  195. logger.info('Unlocked.')
  196. return
  197. else:
  198. logger.info('Authentication failed for device %s. ' \
  199. 'Keeping user "%s" locked down.' % (deviceName, userName))
  200. hpDev = HotPlugDevice(serial)
  201. hpDev.addCallback(authChangeCallback)
  202. logger.info('Watching device "%s" for user "%s"' % (deviceName, userName))
  203. hpDev.run()
  204. udisks = UDisks.Client.new_sync()
  205. udisksObjectManager = udisks.get_object_manager()
  206. sysUsers= []
  207. validUsers = []
  208. def processCheck():
  209. global filelock
  210. filelock=open(os.path.realpath(__file__),'r')
  211. try:
  212. fcntl.flock(filelock,fcntl.LOCK_EX|fcntl.LOCK_NB)
  213. except:
  214. logger.error('Process is already running.')
  215. sys.exit(1)
  216. if os.getuid() != 0:
  217. logger.error('Process must be run as root.')
  218. sys.exit(1)
  219. processCheck()
  220. try:
  221. with open('/etc/passwd', 'r') as f:
  222. for line in f.readlines():
  223. sysUser = re.sub(r'^(.*?):.*', '\\1', line[:-1])
  224. sysUsers.append(sysUser)
  225. f.close()
  226. except:
  227. logger.error('Couldn\'t read system user names from "/etc/passwd". Process can\'t continue.')
  228. sys.exit(1)
  229. logger.info('pamusb-agent up and running.')
  230. for userObj in users:
  231. userId = userObj.get('id')
  232. for sysUser_ in sysUsers:
  233. if (userId == sysUser_ and
  234. userObj not in validUsers):
  235. validUsers.append(userObj)
  236. # logger.error('User %s not found in configuration file' % username)
  237. for user in validUsers:
  238. threading.Thread(
  239. target=userDeviceThread,
  240. args=(user,)
  241. ).start()
  242. if options['daemon'] and os.fork():
  243. sys.exit(0)
  244. def sig_handler(sig, frame):
  245. logger.info('Stopping agent.')
  246. sys.exit(0)
  247. sys_signals = ['SIGINT', 'SIGTERM', 'SIGTSTP', 'SIGTTIN', 'SIGTTOU']
  248. for i in sys_signals:
  249. signal.signal(getattr(signal, i), sig_handler)