#!/usr/bin/env python ''' This is the web interface for a fileserver managing encrypted filesystems. It was originally written in bash/perl. Now a complete rewrite is in progress. So things might be confusing here. Hopefully not for long. :) ''' import CryptoBoxLogger import CryptoBoxContainer import configobj # to read and write the config file import re import os import sys import types import unittest CONF_LOCATIONS = [ "./cryptobox.conf", "~/.cryptobox.conf", "/etc/cryptobox/cryptobox.conf"] class CryptoBoxProps: '''Get and set the properties of a CryptoBox This class contains all available devices that may be accessed. All properties of the cryptobox can be accessed by this class. ''' def __init__(self, conf_file=None): '''read config and fill class variables''' "enable it again - or remove the priv-dropping" #if os.geteuid() != 0: # sys.stderr.write("You need to be root to run this program!\n") # sys.exit(1) if conf_file == None: for f in CONF_LOCATIONS: if os.path.exists(os.path.expanduser(f)): conf_file = os.path.expanduser(f) break else: sys.stderr.write("Could not find a configuration file. I give up.\n") sys.exit(1) try: self.cbxPrefs = configobj.ConfigObj(conf_file) except SyntaxError: sys.stderr.write("Error during parsing of configuration file (%s).\n" % (conf_file, )) sys.exit(1) try: nameDB_file = os.path.join( self.cbxPrefs["Main"]["DataDir"], self.cbxPrefs["Main"]["NameDatabase"]) if os.path.exists(nameDB_file): self.nameDB = configobj.ConfigObj(nameDB_file) else: self.nameDB = configobj.ConfigObj(nameDB_file, create_empty=True) except SyntaxError: sys.stderr.write("Error during parsing of name database file (%s).\n" % (nameDB_file, )) sys.exit(1) self.__cboxUID = int(self.cbxPrefs["System"]["User"]) self.__cboxGID = int(self.cbxPrefs["System"]["Group"]) self.debug = CryptoBoxLogger.CryptoBoxLogger( self.cbxPrefs["Log"]["Level"], self.cbxPrefs["Log"]["Facility"], self.cbxPrefs["Log"]["Destination"]) self.dropPrivileges() self.debugMessage = self.debug.printMessage self.containers = [] for device in self.__getAvailablePartitions(): if self.isDeviceAllowed(device): self.containers.append(CryptoBoxContainer.CryptoBoxContainer(device, self)) def isDeviceAllowed(self, devicename): "check if a device is white-listed for being used as cryptobox containers" "TODO: broken!" allowed = self.cbxPrefs["Main"]["AllowedDevices"] if type(allowed) == types.StringType: allowed = [allowed] for a_dev in allowed: "remove double dots and so on ..." real_device = os.path.realpath(devicename) if a_dev and re.search('^' + a_dev, real_device): return True return False def getContainerList(self, filterType=None, filterName=None): "retrieve the list of all containers of this cryptobox" try: result = self.containers[:] if filterType != None: if filterType in range(len(CryptoBoxContainer.Types)): return [e for e in self.containers if e.getType() == filterType] else: self.logger.debugMessage( "info", "invalid filterType (%d)" % filterType) result.clear() if filterName != None: result = [e for e in self.containers if e.getName() == filterName] return result except AttributeError: return [] def setNameForUUID(self, uuid, name): "assign a name to a uuid in the ContainerNameDatabase" used_uuid = self.getUUIDForName(name) "first remove potential conflicting uuid/name combination" if used_uuid: del self.nameDB[used_uuid] self.nameDB[uuid] = name self.nameDB.write() def getNameForUUID(self, uuid): "get the name belonging to a specified key (usually the UUID of a fs)" try: return self.nameDB[uuid] except KeyError: return None def getUUIDForName(self, name): """ get the key belonging to a value in the ContainerNameDatabase this is the reverse action of 'getNameForUUID' """ for key in self.nameDB.keys(): if self.nameDB[key] == name: return key "the uuid was not found" return None def dropPrivileges(self): "change the effective uid to 'User' specified in section 'System'" "enable it again - or remove the priv-dropping" #if os.getuid() != os.geteuid(): # raise "PrivilegeManager", "we already dropped privileges" os.seteuid(self.__cboxUID) os.setegid(self.__cboxGID) def risePrivileges(self): "regain superuser privileges temporarily - call dropPrivileges afterwards!" "enable it again - or remove the priv-dropping" #if os.getuid() == os.geteuid(): # raise "PrivilegeManager", "we already have superuser privileges" os.seteuid(os.getuid()) os.setegid(os.getgid()) """ ************ internal stuff starts here *********** """ def __getAvailablePartitions(self): "retrieve a list of all available containers" ret_list = [] try: "the following reads all lines of /proc/partitions and adds the mentioned devices" fpart = open("/proc/partitions", "r") try: line = fpart.readline() while line: p_details = line.split() if (len(p_details) == 4): "the following code prevents double entries like /dev/hda and /dev/hda1" (p_major, p_minor, p_size, p_device) = p_details if re.search('^[0-9]*$', p_major) and re.search('^[0-9]*$', p_minor): p_parent = re.sub('[1-9]?[0-9]$', '', p_device) if p_parent == p_device: if [e for e in ret_list if re.search('^' + p_parent + '[1-9]?[0-9]$', e)]: "major partition - its children are already in the list" pass else: "major partition - but there are no children for now" ret_list.append(p_device) else: "minor partition - remove parent if necessary" if p_parent in ret_list: ret_list.remove(p_parent) ret_list.append(p_device) line = fpart.readline() finally: fpart.close() return [self.__getAbsoluteDeviceName(e) for e in ret_list] except IOError: self.debugMessage("Could not read /proc/partitions", "warn") return [] def __getAbsoluteDeviceName(self, shortname): """ returns the absolute file name of a device (e.g.: "hda1" -> "/dev/hda1") this does also work for device mapper devices if the result is non-unique, one arbitrary value is returned""" if re.search('^/', shortname): return shortname default = os.path.join("/dev", shortname) if os.path.exists(default): return default result = self.__findMajorMinorOfDevice(shortname) "if no valid major/minor was found -> exit" if not result: return default (major, minor) = result "for device-mapper devices (major == 254) ..." if major == 254: result = self.__findMajorMinorDeviceName("/dev/mapper", major, minor) if result: return result[0] "now check all files in /dev" result = self.__findMajorMinorDeviceName("/dev", major, minor) if result: return result[0] return default def __findMajorMinorOfDevice(self, device): "return the major/minor numbers of a block device by querying /sys/block/?/dev" if not os.path.exists(os.path.join("/sys/block", device)): return None blockdev_info_file = os.path.join(os.path.join("/sys/block", device), "dev") try: f_blockdev_info = open(blockdev_info_file, "r") blockdev_info = f_blockdev_info.read() f_blockdev_info.close() (str_major, str_minor) = blockdev_info.split(":") "numeric conversion" try: major = int(str_major) minor = int(str_minor) return (major, minor) except ValueError: "unknown device numbers -> stop guessing" return None except IOError: pass def __findMajorMinorDeviceName(self, dir, major, minor): "returns the names of devices with the specified major and minor number" collected = [] try: subdirs = [os.path.join(dir, e) for e in os.listdir(dir) if (not os.path.islink(os.path.join(dir, e))) and os.path.isdir(os.path.join(dir, e))] "do a recursive call to parse the directory tree" for dirs in subdirs: collected.extend(self.__findMajorMinorDeviceName(dirs, major, minor)) "filter all device inodes in this directory" collected.extend([os.path.realpath(os.path.join(dir, e)) for e in os.listdir(dir) if (os.major(os.stat(os.path.join(dir, e)).st_rdev) == major) and (os.minor(os.stat(os.path.join(dir, e)).st_rdev) == minor)]) result = [] for e in collected: if e not in result: result.append(e) return collected except OSError: return [] # *************** test class ********************* class CryptoBoxPropsTest(unittest.TestCase): configFile = "/tmp/cbox-test.conf" nameDBFile = "/tmp/cryptobox_names.db" logFile = "/tmp/cryptobox.log" configContent = """ [Main] AllowedDevices = /dev/loop DefaultVolumePrefix = "Data " DataDir = /tmp NameDatabase = cryptobox_names.db [System] User = 1000 Group = 1000 MountParentDir = /tmp/mnt DefaultCipher = aes-cbc-essiv:sha256 [Log] Level = debug Facility = file Destination = /tmp/cryptobox.log """ def setUp(self): if not os.path.exists("/tmp/mnt"): os.mkdir("/tmp/mnt") fd = open(self.configFile, "w") fd.write(self.configContent) fd.close() def tearDown(self): if os.path.exists("/tmp/mnt"): os.rmdir("/tmp/mnt") if os.path.exists(self.configFile): os.remove(self.configFile) if os.path.exists(self.logFile): os.remove(self.logFile) if os.path.exists(self.nameDBFile): os.remove(self.nameDBFile) def testConfigFile(self): self.assertRaises(KeyError, CryptoBoxProps, "/not/existing/path") CryptoBoxProps(self.configFile) self.assertTrue(os.path.exists(self.nameDBFile)) self.assertTrue(os.path.exists(self.logFile)) def testDeviceCheck(self): cb = CryptoBoxProps(self.configFile) self.assertTrue(cb.isDeviceAllowed("/dev/loop")) self.assertTrue(cb.isDeviceAllowed("/dev/loop1")) self.assertTrue(cb.isDeviceAllowed("/dev/loop/urgd")) self.assertFalse(cb.isDeviceAllowed("/dev/hda")) self.assertFalse(cb.isDeviceAllowed("/dev/loopa/../hda")) self.assertTrue(cb.isDeviceAllowed("/dev/usb/../loop1")) self.assertFalse(cb.isDeviceAllowed("/")) "a lot of tests are still missing - how can we provide a prepared environment?" # ********************* run unittest **************************** if __name__ == "__main__": unittest.main()