2006-08-16 09:17:44 +02:00
|
|
|
#!/usr/bin/env python
|
2006-08-16 13:07:57 +02:00
|
|
|
'''
|
2006-08-17 12:38:05 +02:00
|
|
|
This is the web interface for a fileserver managing encrypted filesystems.
|
2006-08-16 20:17:16 +02:00
|
|
|
|
2006-08-16 13:07:57 +02:00
|
|
|
It was originally written in bash/perl. Now a complete rewrite is in
|
|
|
|
progress. So things might be confusing here. Hopefully not for long.
|
|
|
|
:)
|
|
|
|
'''
|
2006-08-16 20:17:16 +02:00
|
|
|
|
2006-08-16 13:07:57 +02:00
|
|
|
import CryptoBoxLogger
|
|
|
|
import CryptoBoxContainer
|
2006-08-18 22:09:00 +02:00
|
|
|
try:
|
|
|
|
import configobj # to read and write the config file
|
|
|
|
except:
|
|
|
|
print "Could not load configobj module! Try apt-get install python-configobj."
|
2006-08-16 20:17:16 +02:00
|
|
|
import re
|
|
|
|
import os
|
2006-08-17 12:38:05 +02:00
|
|
|
import sys
|
|
|
|
import types
|
2006-08-18 16:00:49 +02:00
|
|
|
import unittest
|
2006-08-17 12:38:05 +02:00
|
|
|
|
|
|
|
|
|
|
|
CONF_LOCATIONS = [
|
|
|
|
"./cryptobox.conf",
|
|
|
|
"~/.cryptobox.conf",
|
|
|
|
"/etc/cryptobox/cryptobox.conf"]
|
2006-08-16 09:17:44 +02:00
|
|
|
|
2006-08-16 13:07:57 +02:00
|
|
|
|
|
|
|
class CryptoBoxProps:
|
|
|
|
'''Get and set the properties of a CryptoBox
|
|
|
|
|
2006-08-17 12:38:05 +02:00
|
|
|
This class contains all available devices that may be accessed.
|
|
|
|
All properties of the cryptobox can be accessed by this class.
|
2006-08-16 13:07:57 +02:00
|
|
|
'''
|
2006-08-16 09:17:44 +02:00
|
|
|
|
2006-08-16 20:17:16 +02:00
|
|
|
|
2006-08-17 12:38:05 +02:00
|
|
|
def __init__(self, conf_file=None):
|
2006-08-16 13:07:57 +02:00
|
|
|
'''read config and fill class variables'''
|
2006-08-18 16:00:49 +02:00
|
|
|
"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)
|
2006-08-17 12:38:05 +02:00
|
|
|
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)
|
2006-08-18 16:00:49 +02:00
|
|
|
else:
|
|
|
|
self.nameDB = configobj.ConfigObj(nameDB_file, create_empty=True)
|
2006-08-17 12:38:05 +02:00
|
|
|
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"])
|
2006-08-18 16:00:49 +02:00
|
|
|
self.__cboxGID = int(self.cbxPrefs["System"]["Group"])
|
2006-08-16 09:17:44 +02:00
|
|
|
self.debug = CryptoBoxLogger.CryptoBoxLogger(
|
2006-08-17 12:38:05 +02:00
|
|
|
self.cbxPrefs["Log"]["Level"],
|
|
|
|
self.cbxPrefs["Log"]["Facility"],
|
2006-08-18 16:00:49 +02:00
|
|
|
self.cbxPrefs["Log"]["Destination"])
|
2006-08-17 12:38:05 +02:00
|
|
|
self.dropPrivileges()
|
|
|
|
self.debugMessage = self.debug.printMessage
|
2006-08-16 20:17:16 +02:00
|
|
|
self.containers = []
|
|
|
|
for device in self.__getAvailablePartitions():
|
|
|
|
if self.isDeviceAllowed(device):
|
2006-08-17 12:38:05 +02:00
|
|
|
self.containers.append(CryptoBoxContainer.CryptoBoxContainer(device, self))
|
2006-08-16 20:17:16 +02:00
|
|
|
|
|
|
|
|
|
|
|
def isDeviceAllowed(self, devicename):
|
|
|
|
"check if a device is white-listed for being used as cryptobox containers"
|
|
|
|
"TODO: broken!"
|
2006-08-17 12:38:05 +02:00
|
|
|
allowed = self.cbxPrefs["Main"]["AllowedDevices"]
|
|
|
|
if type(allowed) == types.StringType: allowed = [allowed]
|
|
|
|
for a_dev in allowed:
|
2006-08-18 16:00:49 +02:00
|
|
|
"remove double dots and so on ..."
|
|
|
|
real_device = os.path.realpath(devicename)
|
|
|
|
if a_dev and re.search('^' + a_dev, real_device): return True
|
2006-08-16 20:17:16 +02:00
|
|
|
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(
|
2006-08-17 12:38:05 +02:00
|
|
|
"info", "invalid filterType (%d)" % filterType)
|
2006-08-16 20:17:16 +02:00
|
|
|
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"
|
2006-08-17 12:38:05 +02:00
|
|
|
if used_uuid: del self.nameDB[used_uuid]
|
|
|
|
self.nameDB[uuid] = name
|
|
|
|
self.nameDB.write()
|
2006-08-16 20:17:16 +02:00
|
|
|
|
|
|
|
|
|
|
|
def getNameForUUID(self, uuid):
|
|
|
|
"get the name belonging to a specified key (usually the UUID of a fs)"
|
|
|
|
try:
|
2006-08-17 12:38:05 +02:00
|
|
|
return self.nameDB[uuid]
|
2006-08-16 20:17:16 +02:00
|
|
|
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' """
|
2006-08-17 12:38:05 +02:00
|
|
|
for key in self.nameDB.keys():
|
|
|
|
if self.nameDB[key] == name: return key
|
2006-08-16 20:17:16 +02:00
|
|
|
"the uuid was not found"
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2006-08-17 12:38:05 +02:00
|
|
|
def dropPrivileges(self):
|
|
|
|
"change the effective uid to 'User' specified in section 'System'"
|
2006-08-18 16:00:49 +02:00
|
|
|
"enable it again - or remove the priv-dropping"
|
|
|
|
#if os.getuid() != os.geteuid():
|
|
|
|
# raise "PrivilegeManager", "we already dropped privileges"
|
2006-08-17 12:38:05 +02:00
|
|
|
os.seteuid(self.__cboxUID)
|
2006-08-18 16:00:49 +02:00
|
|
|
os.setegid(self.__cboxGID)
|
2006-08-17 12:38:05 +02:00
|
|
|
|
|
|
|
|
|
|
|
def risePrivileges(self):
|
|
|
|
"regain superuser privileges temporarily - call dropPrivileges afterwards!"
|
2006-08-18 16:00:49 +02:00
|
|
|
"enable it again - or remove the priv-dropping"
|
|
|
|
#if os.getuid() == os.geteuid():
|
|
|
|
# raise "PrivilegeManager", "we already have superuser privileges"
|
2006-08-17 12:38:05 +02:00
|
|
|
os.seteuid(os.getuid())
|
2006-08-18 16:00:49 +02:00
|
|
|
os.setegid(os.getgid())
|
2006-08-17 12:38:05 +02:00
|
|
|
|
|
|
|
|
2006-08-16 20:17:16 +02:00
|
|
|
""" ************ internal stuff starts here *********** """
|
|
|
|
|
|
|
|
def __getAvailablePartitions(self):
|
|
|
|
"retrieve a list of all available containers"
|
2006-08-16 09:17:44 +02:00
|
|
|
ret_list = []
|
|
|
|
try:
|
2006-08-16 20:17:16 +02:00
|
|
|
"the following reads all lines of /proc/partitions and adds the mentioned devices"
|
2006-08-16 09:17:44 +02:00
|
|
|
fpart = open("/proc/partitions", "r")
|
|
|
|
try:
|
|
|
|
line = fpart.readline()
|
|
|
|
while line:
|
|
|
|
p_details = line.split()
|
|
|
|
if (len(p_details) == 4):
|
2006-08-16 20:17:16 +02:00
|
|
|
"the following code prevents double entries like /dev/hda and /dev/hda1"
|
2006-08-16 09:17:44 +02:00
|
|
|
(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()
|
2006-08-16 20:17:16 +02:00
|
|
|
return [self.__getAbsoluteDeviceName(e) for e in ret_list]
|
2006-08-16 09:17:44 +02:00
|
|
|
except IOError:
|
2006-08-17 12:38:05 +02:00
|
|
|
self.debugMessage("Could not read /proc/partitions", "warn")
|
2006-08-16 09:17:44 +02:00
|
|
|
return []
|
|
|
|
|
|
|
|
|
2006-08-16 20:17:16 +02:00
|
|
|
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")
|
2006-08-16 09:17:44 +02:00
|
|
|
try:
|
2006-08-16 20:17:16 +02:00
|
|
|
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
|
2006-08-16 09:17:44 +02:00
|
|
|
|
|
|
|
|
2006-08-16 20:17:16 +02:00
|
|
|
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 []
|
|
|
|
|
|
|
|
|
2006-08-18 16:00:49 +02:00
|
|
|
|
|
|
|
# *************** test class *********************
|
|
|
|
|
|
|
|
class CryptoBoxPropsTest(unittest.TestCase):
|
2006-08-18 22:09:00 +02:00
|
|
|
import filehandling
|
|
|
|
|
|
|
|
configFile = filehandling.gen_temp_file("cbox-test.conf")
|
|
|
|
nameDBFile = filehandling.gen_temp_file("cryptobox_names.db")
|
|
|
|
logFile = filehandling.gen_temp_file("cryptobox.log")
|
|
|
|
tmpdir = filehandling.gen_temp_dir("cryptobox-mnt")
|
2006-08-18 16:00:49 +02:00
|
|
|
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):
|
2006-08-18 22:09:00 +02:00
|
|
|
if not os.path.exists(tmpdir): os.mkdir(tmpdir)
|
|
|
|
filehandling.write_file(self.configFile,self.configContent)
|
|
|
|
|
2006-08-18 16:00:49 +02:00
|
|
|
|
|
|
|
def tearDown(self):
|
2006-08-18 22:09:00 +02:00
|
|
|
if os.path.exists(tmpdir): os.rmdir(tmpdir)
|
2006-08-18 16:00:49 +02:00
|
|
|
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()
|
|
|
|
|