diff --git a/aif.xsd b/aif.xsd index bf53e8d..a42639a 100644 --- a/aif.xsd +++ b/aif.xsd @@ -452,6 +452,7 @@ + @@ -471,6 +472,14 @@ + + + + diff --git a/aif/constants_fallback.py b/aif/constants_fallback.py index 1f14a93..b0159ef 100644 --- a/aif/constants_fallback.py +++ b/aif/constants_fallback.py @@ -1,4 +1,5 @@ import re +import subprocess # I wish there was a better way to get the supported LUKS ciphers. import uuid ## import parted # https://www.gnu.org/software/parted/api/index.html diff --git a/aif/disk/block.py b/aif/disk/block.py index 3d45ab4..522b404 100644 --- a/aif/disk/block.py +++ b/aif/disk/block.py @@ -22,7 +22,7 @@ class Partition(object): 'primary, extended, or logical partition for msdos partition tables')) aif.disk._common.addBDPlugin('part') self.xml = part_xml - self.id = part_xml.attrib['id'] + self.id = self.xml.attrib['id'] self.table_type = getattr(_BlockDev.PartTableType, tbltype.upper()) if tbltype == 'msdos': # Could technically be _BlockDev.PartTypeReq.NEXT BUT that doesn't *quite* work diff --git a/aif/disk/luks.py b/aif/disk/luks.py index 702b51b..283b141 100644 --- a/aif/disk/luks.py +++ b/aif/disk/luks.py @@ -1,3 +1,6 @@ +import os +import secrets +## from . import _common import aif.disk.block as block import aif.disk.lvm as lvm @@ -7,9 +10,145 @@ import aif.disk.mdadm as mdadm _BlockDev = _common.BlockDev +class LuksSecret(object): + def __init__(self, *args, **kwargs): + _common.addBDPlugin('crypto') + self.passphrase = None + self.size = 4096 + self.path = None + + +class LuksSecretPassphrase(LuksSecret): + def __init__(self, passphrase): + super().__init__() + self.passphrase = passphrase + + +class LuksSecretFile(LuksSecret): + # TODO: might do a little tweaking in a later release to support *reading from* bytes. + def __init__(self, path, passphrase = None, bytesize = 4096): + super().__init__() + self.path = os.path.realpath(path) + self.passphrase = passphrase + self.size = bytesize # only used if passphrase == None + self._genSecret() + + def _genSecret(self): + if not self.passphrase: + # TODO: is secrets.token_bytes safe for *persistent* random data? + self.passphrase = secrets.token_bytes(self.size) + if not isinstance(self.passphrase, bytes): + self.passphrase = self.passphrase.encode('utf-8') + return() + + class LUKS(object): def __init__(self, luks_xml, partobj): self.xml = luks_xml + self.id = self.xml.attrib['id'] + self.name = self.xml.attrib['name'] + self.device = partobj + self.source = self.device.devpath + self.secrets = [] + self.created = False + self.locked = True + if not isinstance(self.device, (block.Disk, + block.Partition, + lvm.LV, + mdadm.Array)): + raise ValueError(('partobj must be of type ' + 'aif.disk.block.Disk, ' + 'aif.disk.block.Partition, ' + 'aif.disk.lvm.LV, or' + 'aif.disk.mdadm.Array')) _common.addBDPlugin('crypto') - self.devpath = None + self.devpath = '/dev/mapper/{0}'.format(self.name) + + def addSecret(self, secretobj): + if not isinstance(secretobj, LuksSecret): + raise ValueError('secretobj must be of type aif.disk.luks.LuksSecret ' + '(aif.disk.luks.LuksSecretPassphrase or ' + 'aif.disk.luks.LuksSecretFile)') + self.secrets.append(secretobj) + return() + + def createSecret(self, secrets_xml = None): + if not secrets_xml: # Find all of them from self + for secret in self.xml.findall('secrets'): + secretobj = None + secrettypes = set() + for s in secret.iterchildren(): + secrettypes.add(s.tag) + if all((('passphrase' in secrettypes), + ('keyFile' in secrettypes))): + # This is safe, because a valid config only has at most one of both types. + kf = secret.find('keyFile') + secretobj = LuksSecretFile(kf.text, # path + passphrase = secret.find('passphrase').text, + bytesize = kf.attrib.get('size', 4096)) # TECHNICALLY should be a no-op. + elif 'passphrase' in secrettypes: + secretobj = LuksSecretPassphrase(secret.find('passphrase').text) + elif 'keyFile' in secrettypes: + kf = secret.find('keyFile') + secretobj = LuksSecretFile(kf.text, + passphrase = None, + bytesize = kf.attrib.get('size', 4096)) + self.secrets.append(secretobj) + else: + secretobj = None + secrettypes = set() + for s in secrets_xml.iterchildren(): + secrettypes.add(s.tag) + if all((('passphrase' in secrettypes), + ('keyFile' in secrettypes))): + # This is safe, because a valid config only has at most one of both types. + kf = secrets_xml.find('keyFile') + secretobj = LuksSecretFile(kf.text, # path + passphrase = secrets_xml.find('passphrase').text, + bytesize = kf.attrib.get('size', 4096)) # TECHNICALLY should be a no-op. + elif 'passphrase' in secrettypes: + secretobj = LuksSecretPassphrase(secrets_xml.find('passphrase').text) + elif 'keyFile' in secrettypes: + kf = secrets_xml.find('keyFile') + secretobj = LuksSecretFile(kf.text, + passphrase = None, + bytesize = kf.attrib.get('size', 4096)) + self.secrets.append(secretobj) + return() + + def create(self): + if not self.secrets: + raise RuntimeError('Cannot create a LUKS volume with no secrets added') + for idx, secret in enumerate(self.secrets): + if idx == 0: + _BlockDev.crypto.luks_format_luks2(self.source, + self.cipher, + self.keysize, + self.passphrase, + self.key_file, + self.min_entropy) + else: + pass # TODO: *add* keyfile/passphrase/whatev. pass + self.created = True + return() + + def lock(self): + if not self.created: + raise RuntimeError('Cannot lock a LUKS volume before it is created') + if self.locked: + return() + + pass + self.locked = True + return() + + def unlock(self, passphrase = None): + if not self.created: + raise RuntimeError('Cannot unlock a LUKS volume before it is created') + if not self.locked: + return() + + pass + self.locked = False + return() diff --git a/aif/disk/mdadm.py b/aif/disk/mdadm.py index 501b1d0..96450c7 100644 --- a/aif/disk/mdadm.py +++ b/aif/disk/mdadm.py @@ -74,7 +74,7 @@ class Member(object): class Array(object): def __init__(self, array_xml, homehost, devpath = None): self.xml = array_xml - self.id = array_xml.attrib['id'] + self.id = self.xml.attrib['id'] self.level = int(self.xml.attrib['level']) if self.level not in aif.constants.MDADM_SUPPORTED_LEVELS: raise ValueError('RAID level must be one of: {0}'.format(', '.join([str(i) diff --git a/aif/disk/mdadm_fallback.py b/aif/disk/mdadm_fallback.py index 64f028d..59ef10f 100644 --- a/aif/disk/mdadm_fallback.py +++ b/aif/disk/mdadm_fallback.py @@ -129,7 +129,7 @@ class Member(object): class Array(object): def __init__(self, array_xml, homehost, devpath = None): self.xml = array_xml - self.id = array_xml.attrib['id'] + self.id = self.xml.attrib['id'] self.level = int(self.xml.attrib['level']) if self.level not in aif.constants.MDADM_SUPPORTED_LEVELS: raise ValueError('RAID level must be one of: {0}'.format(', '.join([str(i)