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.
 
 
 
 
 

813 lines
24 KiB

#
# Copyright 2006 sense.lab e.V.
#
# This file is part of the CryptoBox.
#
# The CryptoBox is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# The CryptoBox is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with the CryptoBox; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
"""Manage a single container of the CryptoBox
"""
__revision__ = "$Id"
import subprocess
import os
import re
import time
from cryptobox.core.exceptions import *
CONTAINERTYPES = {
"unused":0,
"plain":1,
"luks":2,
"swap":3,
}
FSTYPES = {
"plain":["ext3", "ext2", "vfat", "reiserfs"],
"swap":["swap"]}
## we use this marker to make sure, that we do not remove a non-cryptobox directory
## below the mount directory
MOUNT_DIR_MARKER = '_cryptobox_mount_dir_'
class CryptoBoxContainer:
"""Manage a container of the CryptoBox
"""
__dmDir = "/dev/mapper"
def __init__(self, device, cbox):
self.device = device
self.cbox = cbox
self.uuid = None
self.name = None
self.cont_type = None
self.mount = None
self.umount = None
self.attributes = None
self.reset_object()
def get_name(self):
"""Return a humanly readable name for the container.
"""
return self.name
def __set_attributes(self):
"""Define the default attributes of a container.
At least there should be a uuid.
Other attributes may be added by features (e.g. automount).
"""
try:
## is there already an entry in the database?
self.attributes = self.cbox.prefs.volumes_db[self.get_name()]
self.attributes["uuid"] = self.uuid
except KeyError:
## set default values
self.attributes = { "uuid": self.uuid }
self.cbox.prefs.volumes_db[self.get_name()] = self.attributes
def set_name(self, new_name):
"""Define a humanly readable name of this container.
this also manages the name database
"""
old_name = self.get_name()
if new_name == self.name:
return
## renaming is not possible, if the volume is active, as the mountpoint name
## is the same as the volume name
if self.is_mounted():
raise CBVolumeIsActive("the container must not be active during renaming")
if not re.search(r'^[a-zA-Z0-9_\.\- ]+$', new_name):
raise CBInvalidName("the supplied new name contains illegal characters")
## check for another partition with the same name
if self.cbox.get_container_list(filter_name=new_name):
raise CBNameIsInUse("the supplied new name is already in use for anonther partition")
## maybe there a is an entry in the volumes database (but the partition is not active)
try:
## remove possibly existing inactive database item
del self.cbox.prefs.volumes_db[new_name]
except KeyError:
## no entry - so nothing happens
pass
## set new name
self.name = new_name
## remove old database entry
try:
del self.cbox.prefs.volumes_db[old_name]
except KeyError:
pass
## set new volumes database entry
self.cbox.prefs.volumes_db[new_name] = self.attributes
self.cbox.prefs.volumes_db.write()
def get_device(self):
"""Return the device name of the container
e.g.: /dev/hdc1
"""
return self.device
def get_type(self):
"""Return the type (int) of this container.
"""
return self.cont_type
def is_mounted(self):
"""Check if the container is currently mounted.
"""
return os.path.ismount(self.__get_mount_point())
def get_capacity(self):
"""Return the current capacity state of the volume.
the volume may not be mounted
the result is a tuple of values in megabyte:
(size, available, used)
"""
info = os.statvfs(self.__get_mount_point())
return (
int(info.f_bsize*info.f_blocks/1024/1024),
int(info.f_bsize*info.f_bavail/1024/1024),
int(info.f_bsize*(info.f_blocks-info.f_bavail)/1024/1024))
def get_size(self):
"""return the size of the block device (_not_ of the filesystem)
the result is a value in megabyte
an error is indicated by "-1"
"""
import cryptobox.core.tools as cbxtools
return cbxtools.get_blockdevice_size(self.device)
def reset_object(self):
""" recheck the information about this container
this is especially useful after changing the type via 'create' """
self.uuid = self.__get_uuid()
self.cont_type = self.__get_type_of_partition()
self.name = self.__get_name_of_container()
self.__set_attributes()
if self.cont_type == CONTAINERTYPES["luks"]:
self.mount = self.__mount_luks
self.umount = self.__umount_luks
elif self.cont_type == CONTAINERTYPES["plain"]:
self.mount = self.__mount_plain
self.umount = self.__umount_plain
def create(self, cont_type, password=None, fs_type="ext3"):
"""Format a container.
Also set a password for encrypted container.
"""
if not fs_type in FSTYPES["plain"]:
raise CBInvalidType("invalid filesystem type supplied: %s" % str(fs_type))
old_name = self.get_name()
if cont_type == CONTAINERTYPES["luks"]:
self.__create_luks(password, fs_type)
elif cont_type == CONTAINERTYPES["plain"]:
self.__create_plain(fs_type)
else:
raise CBInvalidType("invalid container type (%d) supplied" % (cont_type, ))
## no exception was raised during creation -> we can continue
## reset the properties (encryption state, ...) of the device
self.reset_object()
## restore the old name (must be after reset_object)
try:
self.set_name(old_name)
except CBNameIsInUse:
## failure is okay
pass
def set_busy(self, new_state, time_limit=300):
"""Set the current busy state.
The timelimit is specified in seconds.
"""
if new_state:
self.cbox.busy_devices[self.device] = int(time.time() + time_limit)
else:
try:
if self.cbox.busy_devices[self.device]:
del self.cbox.busy_devices[self.device]
except KeyError:
pass
def is_busy(self):
"""Check the busy state of the container.
"""
if not self.cbox.busy_devices.has_key(self.device):
self.cbox.log.debug("no 'busy' attribute for '%s'" % self.get_name())
return False
## invalid value - can happen after saving and loading the database
if not isinstance(self.cbox.busy_devices[self.device], int):
self.cbox.log.debug("invalid 'busy' attribute for '%s'" % self.get_name())
del db_entry["busy"]
return False
if time.time() >= self.cbox.busy_devices[self.device]:
self.cbox.log.debug("expired 'busy' attribute for '%s'" % self.get_name())
del db_entry["busy"]
return False
## lock is still active
self.cbox.log.debug("active 'busy' attribute for '%s'" % self.get_name())
return True
def change_password(self, oldpw, newpw):
"""Change the password of an encrypted container.
Raises an exception for plaintext container.
"""
if self.cont_type != CONTAINERTYPES["luks"]:
raise CBInvalidType("changing of password is possible only for luks containers")
if not oldpw:
raise CBInvalidPassword("no old password supplied for password change")
if not newpw:
raise CBInvalidPassword("no new password supplied for password change")
## return if new and old passwords are the same
if oldpw == newpw:
return
if self.is_mounted():
raise CBVolumeIsActive("this container is currently active")
devnull = None
try:
devnull = open(os.devnull, "w")
except IOError:
self.cbox.log.warn("Could not open %s" % (os.devnull, ))
## remove any potential open luks mapping
self.__umount_luks()
## create the luks header
proc = subprocess.Popen(
shell = False,
stdin = subprocess.PIPE,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
args = [
self.cbox.prefs["Programs"]["super"],
self.cbox.prefs["Programs"]["CryptoBoxRootActions"],
"cryptsetup",
"luksAddKey",
self.device,
"--batch-mode"])
proc.stdin.write("%s\n%s" % (oldpw, newpw))
(output, errout) = proc.communicate()
if proc.returncode != 0:
error_msg = "Could not add a new luks key: %s - %s" \
% (output.strip(), errout.strip(), )
self.cbox.log.error(error_msg)
raise CBChangePasswordError(error_msg)
## retrieve the key slot we used for unlocking
keys_found = re.search(r'key slot (\d{1,3}) unlocked', output).groups()
if keys_found:
keyslot = int(keys_found[0])
else:
raise CBChangePasswordError("could not get the old key slot")
## remove the old key
proc = subprocess.Popen(
shell = False,
stdin = None,
stdout = devnull,
stderr = subprocess.PIPE,
args = [
self.cbox.prefs["Programs"]["cryptsetup"],
"--batch-mode",
"luksDelKey",
self.device,
"%d" % (keyslot, )])
proc.wait()
if proc.returncode != 0:
error_msg = "Could not remove the old luks key: %s" % (proc.stderr.read().strip(), )
self.cbox.log.error(error_msg)
raise CBChangePasswordError(error_msg)
## ****************** internal stuff *********************
def __get_name_of_container(self):
"""retrieve the name of the container by querying the database
call this function only for the initial setup of the container object"""
found_name = None
for key in self.cbox.prefs.volumes_db.keys():
if self.cbox.prefs.volumes_db[key]["uuid"] == self.uuid:
found_name = key
if found_name:
return found_name
## there is no name defined for this uuid - we will propose a good one
prefix = self.cbox.prefs["Main"]["DefaultVolumePrefix"]
unused_found = False
counter = 1
while not unused_found:
guess = prefix + str(counter)
if self.cbox.prefs.volumes_db.has_key(guess):
counter += 1
else:
unused_found = True
return guess
def __get_uuid(self):
"""Retrieve the uuid of the container device.
"""
if self.__get_type_of_partition() == CONTAINERTYPES["luks"]:
guess = self.__get_luks_uuid()
else:
guess = self.__get_non_luks_uuid()
## did we get a valid value?
if guess:
return guess
else:
## emergency default value
return self.device.replace(os.path.sep, "_")
def __get_luks_uuid(self):
"""get uuid for luks devices"""
proc = subprocess.Popen(
shell = False,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
args = [self.cbox.prefs["Programs"]["cryptsetup"],
"luksUUID",
self.device])
(stdout, stderr) = proc.communicate()
if proc.returncode != 0:
self.cbox.log.info("could not retrieve luks uuid (%s): %s",
(self.device, stderr.strip()))
return None
return stdout.strip()
def __get_non_luks_uuid(self):
"""return UUID for ext2/3 and vfat filesystems"""
try:
devnull = open(os.devnull, "w")
except IOError:
self.cbox.log.warn("Could not open %s" % (os.devnull, ))
proc = subprocess.Popen(
shell=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
args=[self.cbox.prefs["Programs"]["blkid"],
"-s", "UUID",
"-o", "value",
"-c", os.devnull,
"-w", os.devnull,
self.device])
(stdout, stderr) = proc.communicate()
devnull.close()
## execution failed?
if proc.returncode != 0:
self.cbox.log.info("retrieving of partition type (" + str(self.device) \
+ ") via 'blkid' failed: " + str(stderr.strip()) \
+ " - maybe it is encrypted?")
return None
## return output of blkid
return stdout.strip()
def __get_type_of_partition(self):
"""Retrieve the type of the given partition.
see cryptobox.core.container.CONTAINERTYPES
"""
if self.__is_luks_partition():
return CONTAINERTYPES["luks"]
type_of_partition = self.__get_type_id_of_partition()
if type_of_partition in FSTYPES["plain"]:
return CONTAINERTYPES["plain"]
if type_of_partition in FSTYPES["swap"]:
return CONTAINERTYPES["swap"]
return CONTAINERTYPES["unused"]
def __get_type_id_of_partition(self):
"returns the type of the partition (see 'man blkid')"
devnull = None
proc = subprocess.Popen(
shell = False,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
args = [ self.cbox.prefs["Programs"]["blkid"],
"-s", "TYPE",
"-o", "value",
"-c", os.devnull,
"-w", os.devnull,
self.device ])
(stdout, stder) = proc.communicate()
if proc.returncode == 0:
## we found a uuid
return stdout.strip()
elif proc.returncode == 2:
## failed to find the attribute - no problem
return None
else:
## something strange happened
self.cbox.log.warn("retrieving of partition type via 'blkid' failed: %s" % \
(stderr.strip(), ))
return None
def __is_luks_partition(self):
"check if the given device is a luks partition"
devnull = None
try:
devnull = open(os.devnull, "w")
except IOError:
self.cbox.log.warn("Could not open %s" % (os.devnull, ))
proc = subprocess.Popen(
shell = False,
stdin = None,
stdout = devnull,
stderr = devnull,
args = [
self.cbox.prefs["Programs"]["cryptsetup"],
"--batch-mode",
"isLuks",
self.device])
proc.wait()
devnull.close()
return proc.returncode == 0
def __get_mount_point(self):
"return the name of the mountpoint of this volume"
return os.path.join(self.cbox.prefs["Locations"]["MountParentDir"], self.name)
def __mount_luks(self, password):
"mount a luks partition"
if not password:
raise CBInvalidPassword("no password supplied for luksOpen")
if self.is_mounted():
raise CBVolumeIsActive("this container is already active")
self.__umount_luks()
try:
devnull = open(os.devnull, "w")
except IOError:
self.cbox.log.warn("Could not open %s" % (os.devnull, ))
self.__clean_mount_dirs()
if not os.path.exists(self.__get_mount_point()):
self.__create_mount_directory(self.__get_mount_point())
if not os.path.exists(self.__get_mount_point()):
err_msg = "Could not create mountpoint (%s)" % (self.__get_mount_point(), )
self.cbox.log.error(err_msg)
raise CBMountError(err_msg)
self.cbox.send_event_notification("premount", self.__get_event_args())
proc = subprocess.Popen(
shell = False,
stdin = subprocess.PIPE,
stdout = devnull,
stderr = subprocess.PIPE,
args = [
self.cbox.prefs["Programs"]["super"],
self.cbox.prefs["Programs"]["CryptoBoxRootActions"],
"cryptsetup",
"luksOpen",
self.device,
self.name,
"--batch-mode"])
proc.stdin.write(password)
(output, errout) = proc.communicate()
if proc.returncode != 0:
err_msg = "Could not open the luks mapping: %s" % (errout.strip(), )
self.cbox.log.warn(err_msg)
raise CBMountError(err_msg)
proc = subprocess.Popen(
shell = False,
stdin = None,
stdout = devnull,
stderr = subprocess.PIPE,
args = [
self.cbox.prefs["Programs"]["super"],
self.cbox.prefs["Programs"]["CryptoBoxRootActions"],
"mount",
os.path.join(self.__dmDir, self.name),
self.__get_mount_point()])
proc.wait()
if proc.returncode != 0:
err_msg = "Could not mount the filesystem: %s" % (proc.stderr.read().strip(), )
self.cbox.log.warn(err_msg)
raise CBMountError(err_msg)
devnull.close()
self.cbox.send_event_notification("postmount", self.__get_event_args())
def __umount_luks(self):
"umount a luks partition"
devnull = None
try:
devnull = open(os.devnull, "w")
except IOError:
self.cbox.log.warn("Could not open %s" % (os.devnull, ))
self.cbox.send_event_notification("preumount", self.__get_event_args())
if self.is_mounted():
proc = subprocess.Popen(
shell = False,
stdin = None,
stdout = devnull,
stderr = subprocess.PIPE,
args = [
self.cbox.prefs["Programs"]["super"],
self.cbox.prefs["Programs"]["CryptoBoxRootActions"],
"umount",
self.__get_mount_point()])
proc.wait()
if proc.returncode != 0:
err_msg = "Could not umount the filesystem: %s" % (proc.stderr.read().strip(), )
self.cbox.log.warn(err_msg)
raise CBUmountError(err_msg)
if os.path.exists(os.path.join(self.__dmDir, self.name)):
proc = subprocess.Popen(
shell = False,
stdin = None,
stdout = devnull,
stderr = subprocess.PIPE,
args = [
self.cbox.prefs["Programs"]["super"],
self.cbox.prefs["Programs"]["CryptoBoxRootActions"],
"cryptsetup",
"luksClose",
self.name,
"--batch-mode"])
proc.wait()
if proc.returncode != 0:
err_msg = "Could not remove the luks mapping: %s" % (proc.stderr.read().strip(), )
self.cbox.log.warn(err_msg)
raise CBUmountError(err_msg)
devnull.close()
self.cbox.send_event_notification("postumount", self.__get_event_args())
def __mount_plain(self):
"mount a plaintext partition"
if self.is_mounted():
raise CBVolumeIsActive("this container is already active")
devnull = None
try:
devnull = open(os.devnull, "w")
except IOError:
self.cbox.log.warn("Could not open %s" % (os.devnull, ))
self.__clean_mount_dirs()
if not os.path.exists(self.__get_mount_point()):
self.__create_mount_directory(self.__get_mount_point())
if not os.path.exists(self.__get_mount_point()):
err_msg = "Could not create mountpoint (%s)" % (self.__get_mount_point(), )
self.cbox.log.error(err_msg)
raise CBMountError(err_msg)
self.cbox.send_event_notification("premount", self.__get_event_args())
proc = subprocess.Popen(
shell = False,
stdin = None,
stdout = devnull,
stderr = subprocess.PIPE,
args = [
self.cbox.prefs["Programs"]["super"],
self.cbox.prefs["Programs"]["CryptoBoxRootActions"],
"mount",
self.device,
self.__get_mount_point()])
proc.wait()
if proc.returncode != 0:
err_msg = "Could not mount the filesystem: %s" % (proc.stderr.read().strip(), )
self.cbox.log.warn(err_msg)
raise CBMountError(err_msg)
devnull.close()
self.cbox.send_event_notification("postmount", self.__get_event_args())
def __umount_plain(self):
"umount a plaintext partition"
if not self.is_mounted():
self.cbox.log.info("trying to umount while volume (%s) is mounted" % \
self.get_device())
return
devnull = None
try:
devnull = open(os.devnull, "w")
except IOError:
self.cbox.log.warn("Could not open %s" % (os.devnull, ))
self.cbox.send_event_notification("preumount", self.__get_event_args())
proc = subprocess.Popen(
shell = False,
stdin = None,
stdout = devnull,
stderr = subprocess.PIPE,
args = [
self.cbox.prefs["Programs"]["super"],
self.cbox.prefs["Programs"]["CryptoBoxRootActions"],
"umount",
self.__get_mount_point()])
proc.wait()
if proc.returncode != 0:
err_msg = "Could not umount the filesystem: %s" % (proc.stderr.read().strip(), )
self.cbox.log.warn(err_msg)
raise CBUmountError(err_msg)
devnull.close()
self.cbox.send_event_notification("postumount", self.__get_event_args())
def __create_plain(self, fs_type="ext3"):
"make a plaintext partition"
import threading
if self.is_mounted():
raise CBVolumeIsActive(
"deactivate the partition before filesystem initialization")
def format():
import os
old_name = self.get_name()
self.set_busy(True, 600)
self.cbox.log.debug("Turn the busy flag on: %s" % self.device)
## give the main thread a chance to continue
child_pid = os.fork()
if child_pid == 0:
proc = subprocess.Popen(
shell = False,
stdin = None,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
args = [
self.cbox.prefs["Programs"]["nice"],
self.cbox.prefs["Programs"]["mkfs"],
"-t", fs_type, self.device])
(stdout, sterr) = proc.communicate()
## for to allow error detection
if proc.returncode == 0:
time.sleep(5)
## skip cleanup stuff (as common for sys.exit)
os._exit(0)
else:
os.waitpid(child_pid, 0)
self.set_name(old_name)
self.set_busy(False)
self.cbox.log.debug("Turn the busy flag off: %s" % self.device)
bg_task = threading.Thread(target=format)
bg_task.start()
time.sleep(3)
## if the thread exited very fast, then it failed
if not bg_task.isAlive():
raise CBCreateError("Failed to initilize device: %s" % self.device)
def __create_luks(self, password, fs_type="ext3"):
"""Create a luks partition.
"""
import threading
if not password:
raise CBInvalidPassword("no password supplied for new luks mapping")
if self.is_mounted():
raise CBVolumeIsActive("deactivate the partition before filesystem initialization")
## remove any potential open luks mapping
self.__umount_luks()
## create the luks header
proc = subprocess.Popen(
shell = False,
stdin = subprocess.PIPE,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
args = [
self.cbox.prefs["Programs"]["super"],
self.cbox.prefs["Programs"]["CryptoBoxRootActions"],
"cryptsetup",
"luksFormat",
self.device,
"--batch-mode",
"--cipher", self.cbox.prefs["Main"]["DefaultCipher"],
"--iter-time", "2000"])
proc.stdin.write(password)
(output, errout) = proc.communicate()
if proc.returncode != 0:
err_msg = "Could not create the luks header: %s" % (errout.strip(), )
self.cbox.log.error(err_msg)
raise CBCreateError(err_msg)
## open the luks container for mkfs
proc = subprocess.Popen(
shell = False,
stdin = subprocess.PIPE,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
args = [
self.cbox.prefs["Programs"]["super"],
self.cbox.prefs["Programs"]["CryptoBoxRootActions"],
"cryptsetup",
"luksOpen",
self.device,
self.name,
"--batch-mode"])
proc.stdin.write(password)
(output, errout) = proc.communicate()
if proc.returncode != 0:
err_msg = "Could not open the new luks mapping: %s" % (errout.strip(), )
self.cbox.log.error(err_msg)
raise CBCreateError(err_msg)
def format_luks():
import os
old_name = self.get_name()
self.set_busy(True, 600)
self.cbox.log.debug("Turn the busy flag on: %s" % self.device)
child_pid = os.fork()
if child_pid == 0:
## make the filesystem
proc = subprocess.Popen(
shell = False,
stdin = None,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
args = [
self.cbox.prefs["Programs"]["nice"],
self.cbox.prefs["Programs"]["mkfs"],
"-t", fs_type,
os.path.join(self.__dmDir, self.name)])
(stdou, stderr) = proc.communicate()
## wait to allow error detection
if proc.returncode == 0:
time.sleep(5)
## skip cleanup stuff (as common for sys.exit)
os._exit(0)
else:
os.waitpid(child_pid, 0)
self.set_name(old_name)
self.set_busy(False)
self.cbox.log.debug("Turn the busy flag off: %s" % self.device)
## remove the mapping - for every exit status
self.__umount_luks()
bg_task = threading.Thread(target=format_luks)
bg_task.setDaemon(True)
bg_task.start()
time.sleep(3)
## if the thread exited very fast, then it failed
if not bg_task.isAlive():
raise CBCreateError("Failed to initilize device: %s" % self.device)
def __clean_mount_dirs(self):
""" remove all unnecessary subdirs of the mount parent directory
this should be called for every (u)mount """
subdirs = os.listdir(self.cbox.prefs["Locations"]["MountParentDir"])
for one_dir in subdirs:
abs_dir = os.path.join(self.cbox.prefs["Locations"]["MountParentDir"], one_dir)
if (not os.path.islink(abs_dir)) \
and os.path.isdir(abs_dir) \
and (not os.path.ismount(abs_dir)) \
and (os.path.isfile(os.path.join(abs_dir,MOUNT_DIR_MARKER))) \
and (len(os.listdir(abs_dir)) == 1):
try:
os.remove(os.path.join(abs_dir, MOUNT_DIR_MARKER))
os.rmdir(abs_dir)
except OSError, err_msg:
## we do not care too much about unclean cleaning ...
self.cbox.log.info("failed to clean a mountpoint (%s): %s" % \
(abs_dir, str(err_msg)))
def __create_mount_directory(self, dirname):
"""create and mark a mount directory
this marking helps to remove old mountdirs safely"""
os.mkdir(dirname)
try:
mark_file = file(os.path.join(dirname, MOUNT_DIR_MARKER), "w")
mark_file.close()
except OSError, err_msg:
## we do not care too much about the marking
self.cbox.log.info("failed to mark a mountpoint (%s): %s" % (dirname, str(err_msg)))
def __get_event_args(self):
"""Return an array of arguments for event scripts.
for now supported: pre/post-mount/umount events
"""
type_text = [e for e in CONTAINERTYPES.keys()
if CONTAINERTYPES[e] == self.get_type()][0]
return [self.get_device(), self.get_name(), type_text, self.__get_mount_point()]