summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbrent s <r00t@square-r00t.net>2019-11-10 01:37:15 -0500
committerbrent s <r00t@square-r00t.net>2019-11-10 01:37:15 -0500
commitf68069a25ed5128fe66969f7851195f9faa3c6d6 (patch)
tree0a01ae933278245b09dc0ba233a599dbad6220ac
parenta1c126847cc06d07d36019829e88adf7118f5484 (diff)
downloadAIF-NG-f68069a25ed5128fe66969f7851195f9faa3c6d6.tar.xz
i'm pretty sure luks non-gi is now done
-rw-r--r--aif.xsd3
-rw-r--r--aif/constants_fallback.py1
-rw-r--r--aif/disk/luks.py77
-rw-r--r--aif/disk/luks_fallback.py249
-rw-r--r--aif/disk/mdadm.py2
-rw-r--r--aif/disk/mdadm_fallback.py4
6 files changed, 320 insertions, 16 deletions
diff --git a/aif.xsd b/aif.xsd
index 18cce0e..f9b3e58 100644
--- a/aif.xsd
+++ b/aif.xsd
@@ -442,11 +442,12 @@
<xs:element name="luks" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:sequence>
+ <!-- TODO: add support for custom flags/opts? -->
<xs:element name="luksDev" minOccurs="1" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence>
<xs:element name="secrets" minOccurs="1"
- maxOccurs="unbounded">
+ maxOccurs="10">
<xs:complexType>
<xs:sequence minOccurs="1" maxOccurs="unbounded">
<xs:element name="passphrase" minOccurs="0"
diff --git a/aif/constants_fallback.py b/aif/constants_fallback.py
index b0159ef..2217f03 100644
--- a/aif/constants_fallback.py
+++ b/aif/constants_fallback.py
@@ -12,6 +12,7 @@ EXTERNAL_DEPS = ['blkinfo',
'gpg',
'lxml',
'mdstat',
+ 'parse',
'passlib',
'psutil',
'pyparted',
diff --git a/aif/disk/luks.py b/aif/disk/luks.py
index 283b141..2b2c884 100644
--- a/aif/disk/luks.py
+++ b/aif/disk/luks.py
@@ -1,5 +1,6 @@
import os
import secrets
+import uuid
##
from . import _common
import aif.disk.block as block
@@ -63,6 +64,7 @@ class LUKS(object):
'aif.disk.mdadm.Array'))
_common.addBDPlugin('crypto')
self.devpath = '/dev/mapper/{0}'.format(self.name)
+ self.info = None
def addSecret(self, secretobj):
if not isinstance(secretobj, LuksSecret):
@@ -117,19 +119,24 @@ class LUKS(object):
return()
def create(self):
+ if self.created:
+ return()
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)
+ # TODO: add support for custom parameters for below?
+ _BlockDev.crypto.luks_format_luks2_blob(self.source,
+ None, # cipher (use default)
+ 0, # keysize (use default)
+ secret.passphrase, # passphrase
+ 0, # minimum entropy (use default)
+ _BlockDev.CryptoLUKSVersion.LUKS2, # LUKS version
+ None) # extra args
else:
- pass # TODO: *add* keyfile/passphrase/whatev.
- pass
+ _BlockDev.crypto.luks_add_key_blob(self.source,
+ self.secrets[0].passphrase,
+ secret.passphrase)
self.created = True
return()
@@ -138,8 +145,7 @@ class LUKS(object):
raise RuntimeError('Cannot lock a LUKS volume before it is created')
if self.locked:
return()
-
- pass
+ _BlockDev.crypto.luks_close(self.name)
self.locked = True
return()
@@ -148,7 +154,54 @@ class LUKS(object):
raise RuntimeError('Cannot unlock a LUKS volume before it is created')
if not self.locked:
return()
-
- pass
+ _BlockDev.crypto.luks_open_blob(self.source,
+ self.name,
+ self.secrets[0].passphrase,
+ False) # read-only
self.locked = False
return()
+
+ def updateInfo(self):
+ if self.locked:
+ raise RuntimeError('Must be unlocked to gather info')
+ info = {}
+ _info = _BlockDev.crypto.luks_info(self.devpath)
+ for k in dir(_info):
+ if k.startswith('_'):
+ continue
+ elif k in ('copy', ):
+ continue
+ v = getattr(_info, k)
+ if k == 'uuid':
+ v = uuid.UUID(hex = v)
+ info[k] = v
+ info['_cipher'] = '{cipher}-{mode}'.format(**info)
+ self.info = info
+ return()
+
+ def writeConf(self, conf = '/etc/crypttab'):
+ if not self.secrets:
+ raise RuntimeError('secrets must be added before the configuration can be written')
+ conf = os.path.realpath(conf)
+ with open(conf, 'r') as fh:
+ conflines = fh.read().splitlines()
+ # Get UUID
+ disk_uuid = None
+ uuid_dir = '/dev/disk/by-uuid'
+ for u in os.listdir(uuid_dir):
+ d = os.path.join(uuid_dir, u)
+ if os.path.realpath(d) == self.source:
+ disk_uuid = u
+ if disk_uuid:
+ identifer = 'UUID={0}'.format(disk_uuid)
+ else:
+ # This is *not* ideal, but better than nothing.
+ identifer = self.source
+ primary_key = self.secrets[0]
+ luksinfo = '{0}\t{1}\t{2}\tluks'.format(self.name,
+ identifer,
+ (primary_key.path if primary_key.path else '-'))
+ if luksinfo not in conflines:
+ with open(conf, 'a') as fh:
+ fh.write('{0}\n'.format(luksinfo))
+ return()
diff --git a/aif/disk/luks_fallback.py b/aif/disk/luks_fallback.py
index e0bcf89..0ab05cf 100644
--- a/aif/disk/luks_fallback.py
+++ b/aif/disk/luks_fallback.py
@@ -1,10 +1,255 @@
+import os
+import re
+import secrets
+import subprocess
+import tempfile
+import uuid
+##
+import parse
+##
import aif.disk.block_fallback as block
import aif.disk.lvm_fallback as lvm
import aif.disk.mdadm_fallback as mdadm
+class LuksSecret(object):
+ def __init__(self, *args, **kwargs):
+ 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.devpath = None
- pass
+ 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'))
+ self.devpath = '/dev/mapper/{0}'.format(self.name)
+ self.info = None
+
+ 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 self.created:
+ return()
+ 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:
+ # TODO: add support for custom parameters for below?
+ # TODO: logging
+ cmd = ['cryptsetup',
+ '--batch-mode',
+ 'luksFormat',
+ '--type', 'luks2',
+ '--key-file', '-',
+ self.source]
+ subprocess.run(cmd, input = secret.passphrase)
+ else:
+ tmpfile = tempfile.mkstemp()
+ with open(tmpfile[1], 'wb') as fh:
+ fh.write(secret.passphrase)
+ cmd = ['cryptsetup',
+ '--batch-mode',
+ 'luksAdd',
+ '--type', 'luks2',
+ '--key-file', '-',
+ self.source,
+ tmpfile[1]]
+ subprocess.run(cmd, input = self.secrets[0].passphrase)
+ os.remove(tmpfile[1])
+ 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()
+ # TODO: logging
+ cmd = ['cryptsetup',
+ '--batch-mode',
+ 'luksClose',
+ self.name]
+ subprocess.run(cmd)
+ 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()
+ cmd = ['cryptsetup',
+ '--batch-mode',
+ 'luksOpen',
+ '--key-file', '-',
+ self.source,
+ self.name]
+ subprocess.run(cmd, input = self.secrets[0].passphrase)
+ self.locked = False
+ return()
+
+ def updateInfo(self):
+ if self.locked:
+ raise RuntimeError('Must be unlocked to gather info')
+ info = {}
+ cmd = ['cryptsetup',
+ '--batch-mode',
+ 'luksDump',
+ self.source]
+ _info = subprocess.run(cmd, stdout = subprocess.PIPE).stdout.decode('utf-8')
+ k = None
+ # I wish there was a better way to do this but I sure as heck am not writing a regex to do it.
+ # https://pypi.org/project/parse/
+ _tpl = ('LUKS header information\nVersion: {header_ver}\nEpoch: {epoch_ver}\n'
+ 'Metadata area: {metadata_pos} [bytes]\nKeyslots area: {keyslots_pos} [bytes]\n'
+ 'UUID: {uuid}\nLabel: {label}\nSubsystem: {subsystem}\n'
+ 'Flags: {flags}\n\nData segments:\n 0: crypt\n '
+ 'offset: {offset_bytes} [bytes]\n length: {crypt_length}\n '
+ 'cipher: {crypt_cipher}\n sector: {sector_size} [bytes]\n\nKeyslots:\n 0: luks2\n '
+ 'Key: {key_size} bits\n Priority: {priority}\n '
+ 'Cipher: {keyslot_cipher}\n Cipher key: {cipher_key_size} bits\n '
+ 'PBKDF: {pbkdf}\n Time cost: {time_cost}\n Memory: {memory}\n '
+ 'Threads: {threads}\n Salt: {key_salt} \n AF stripes: {af_stripes}\n '
+ 'AF hash: {af_hash}\n Area offset:{keyslot_offset} [bytes]\n '
+ 'Area length:{keyslot_length} [bytes]\n Digest ID: {keyslot_id}\nTokens:\nDigests:\n '
+ '0: pbkdf2\n Hash: {token_hash}\n Iterations: {token_iterations}\n '
+ 'Salt: {token_salt}\n Digest: {token_digest}\n\n')
+ info = parse.parse(_tpl, _info).named
+ for k, v in info.items():
+ # Technically we can do this in the _tpl string, but it's hard to visually parse.
+ if k in ('af_stripes', 'cipher_key_size', 'epoch_ver', 'header_ver', 'key_size', 'keyslot_id',
+ 'keyslot_length', 'keyslot_offset', 'keyslots_pos', 'memory', 'metadata_pos', 'offset_bytes',
+ 'sector_size', 'threads', 'time_cost', 'token_iterations'):
+ v = int(v)
+ elif k in ('key_salt', 'token_digest', 'token_salt'):
+ v = bytes.fromhex(re.sub(r'\s+', '', v))
+ elif k in ('label', 'subsystem'):
+ if re.search(r'\(no\s+', v.lower()):
+ v = None
+ elif k == 'flags':
+ if v.lower() == '(no flags)':
+ v = []
+ else:
+ # Space-separated or comma-separated? TODO.
+ v = [i.strip() for i in v.split() if i.strip() != '']
+ elif k == 'uuid':
+ v = uuid.UUID(hex = v)
+ self.info = info
+ return()
+
+ def writeConf(self, conf = '/etc/crypttab'):
+ if not self.secrets:
+ raise RuntimeError('secrets must be added before the configuration can be written')
+ conf = os.path.realpath(conf)
+ with open(conf, 'r') as fh:
+ conflines = fh.read().splitlines()
+ # Get UUID
+ disk_uuid = None
+ uuid_dir = '/dev/disk/by-uuid'
+ for u in os.listdir(uuid_dir):
+ d = os.path.join(uuid_dir, u)
+ if os.path.realpath(d) == self.source:
+ disk_uuid = u
+ if disk_uuid:
+ identifer = 'UUID={0}'.format(disk_uuid)
+ else:
+ # This is *not* ideal, but better than nothing.
+ identifer = self.source
+ primary_key = self.secrets[0]
+ luksinfo = '{0}\t{1}\t{2}\tluks'.format(self.name,
+ identifer,
+ (primary_key.path if primary_key.path else '-'))
+ if luksinfo not in conflines:
+ with open(conf, 'a') as fh:
+ fh.write('{0}\n'.format(luksinfo))
+ return()
diff --git a/aif/disk/mdadm.py b/aif/disk/mdadm.py
index 96450c7..072f503 100644
--- a/aif/disk/mdadm.py
+++ b/aif/disk/mdadm.py
@@ -1,4 +1,5 @@
import datetime
+import os
import re
import uuid
##
@@ -191,6 +192,7 @@ class Array(object):
return()
def writeConf(self, conf = '/etc/mdadm.conf'):
+ conf = os.path.realpath(conf)
with open(conf, 'r') as fh:
conflines = fh.read().splitlines()
arrayinfo = ('ARRAY '
diff --git a/aif/disk/mdadm_fallback.py b/aif/disk/mdadm_fallback.py
index 59ef10f..daa90e6 100644
--- a/aif/disk/mdadm_fallback.py
+++ b/aif/disk/mdadm_fallback.py
@@ -1,6 +1,6 @@
import copy
import datetime
-import math
+import os
import re
import subprocess
import uuid
@@ -126,6 +126,7 @@ class Member(object):
self._parseDeviceBlock()
return()
+
class Array(object):
def __init__(self, array_xml, homehost, devpath = None):
self.xml = array_xml
@@ -230,6 +231,7 @@ class Array(object):
return()
def writeConf(self, conf = '/etc/mdadm.conf'):
+ conf = os.path.realpath(conf)
with open(conf, 'r') as fh:
conflines = fh.read().splitlines()
# TODO: logging