summaryrefslogtreecommitdiffstats
path: root/ipaclient
diff options
context:
space:
mode:
authorJan Cholasta <jcholast@redhat.com>2016-04-28 09:46:03 +0200
committerJan Cholasta <jcholast@redhat.com>2016-06-03 09:00:34 +0200
commit327d95296a5b28179469c20ec5f98dba3c333017 (patch)
treecb6fffa8b84a0cd1221821c4d21ab4b903f0c6fe /ipaclient
parentf1ad3e67ae765ed6bf72fc861ffb47f1d6637c47 (diff)
downloadfreeipa-327d95296a5b28179469c20ec5f98dba3c333017.tar.gz
freeipa-327d95296a5b28179469c20ec5f98dba3c333017.tar.xz
freeipa-327d95296a5b28179469c20ec5f98dba3c333017.zip
ipalib: move client-side plugins to ipaclient
Move the rpcclient backend and commands which are executed on the client to ipaclient.plugins. https://fedorahosted.org/freeipa/ticket/4739 Reviewed-By: David Kupka <dkupka@redhat.com>
Diffstat (limited to 'ipaclient')
-rw-r--r--ipaclient/plugins/automount.py227
-rw-r--r--ipaclient/plugins/otptoken.py110
-rw-r--r--ipaclient/plugins/otptoken_yubikey.py152
-rw-r--r--ipaclient/plugins/rpcclient.py53
-rw-r--r--ipaclient/plugins/vault.py936
5 files changed, 1478 insertions, 0 deletions
diff --git a/ipaclient/plugins/automount.py b/ipaclient/plugins/automount.py
new file mode 100644
index 000000000..096315818
--- /dev/null
+++ b/ipaclient/plugins/automount.py
@@ -0,0 +1,227 @@
+# Authors:
+# Rob Crittenden <rcritten@redhat.com>
+# Pavel Zuna <pzuna@redhat.com>
+#
+# Copyright (C) 2008 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program 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 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+import six
+
+from ipalib import api, errors
+from ipalib import Flag, Str
+from ipalib.frontend import Command
+from ipalib.plugable import Registry
+from ipalib import _
+
+if six.PY3:
+ unicode = str
+
+register = Registry()
+
+DEFAULT_MAPS = (u'auto.direct', )
+DEFAULT_KEYS = (u'/-', )
+
+
+@register()
+class automountlocation_import(Command):
+ __doc__ = _('Import automount files for a specific location.')
+
+ takes_args = (
+ Str('masterfile',
+ label=_('Master file'),
+ doc=_('Automount master file.'),
+ ),
+ )
+
+ takes_options = (
+ Flag('continue?',
+ cli_name='continue',
+ doc=_('Continuous operation mode. Errors are reported but the process continues.'),
+ ),
+ )
+
+ def get_args(self):
+ for arg in self.api.Command.automountlocation_show.args():
+ yield arg
+ for arg in super(automountlocation_import, self).get_args():
+ yield arg
+
+ def __read_mapfile(self, filename):
+ try:
+ fp = open(filename, 'r')
+ map = fp.readlines()
+ fp.close()
+ except IOError as e:
+ if e.errno == 2:
+ raise errors.NotFound(
+ reason=_('File %(file)s not found') % {'file': filename}
+ )
+ else:
+ raise
+ return map
+
+ def forward(self, *args, **options):
+ """
+ The basic idea is to read the master file and create all the maps
+ we need, then read each map file and add all the keys for the map.
+ """
+ location = self.api.Command['automountlocation_show'](args[0])
+
+ result = {'maps':[], 'keys':[], 'skipped':[], 'duplicatekeys':[], 'duplicatemaps':[]}
+ maps = {}
+ master = self.__read_mapfile(args[1])
+ for m in master:
+ if m.startswith('#'):
+ continue
+ m = m.rstrip()
+ if m.startswith('+'):
+ result['skipped'].append([m,args[1]])
+ continue
+ if len(m) == 0:
+ continue
+ am = m.split(None)
+ if len(am) < 2:
+ continue
+
+ if am[1].startswith('/'):
+ mapfile = am[1].replace('"','')
+ am[1] = os.path.basename(am[1])
+ maps[am[1]] = mapfile
+
+ # Add a new key to the auto.master map for the new map file
+ try:
+ api.Command['automountkey_add'](
+ args[0],
+ u'auto.master',
+ automountkey=unicode(am[0]),
+ automountinformation=unicode(' '.join(am[1:])))
+ result['keys'].append([am[0], u'auto.master'])
+ except errors.DuplicateEntry as e:
+ if unicode(am[0]) in DEFAULT_KEYS:
+ # ignore conflict when the key was pre-created by the framework
+ pass
+ elif options.get('continue', False):
+ result['duplicatekeys'].append(am[0])
+ else:
+ raise errors.DuplicateEntry(
+ message=_('key %(key)s already exists') % dict(
+ key=am[0]))
+ # Add the new map
+ if not am[1].startswith('-'):
+ try:
+ api.Command['automountmap_add'](args[0], unicode(am[1]))
+ result['maps'].append(am[1])
+ except errors.DuplicateEntry as e:
+ if unicode(am[1]) in DEFAULT_MAPS:
+ # ignore conflict when the map was pre-created by the framework
+ pass
+ elif options.get('continue', False):
+ result['duplicatemaps'].append(am[0])
+ else:
+ raise errors.DuplicateEntry(
+ message=_('map %(map)s already exists') % dict(
+ map=am[1]))
+
+ # Now iterate over the map files and add the keys. To handle
+ # continuation lines I'll make a pass through it to skip comments
+ # etc and also to combine lines.
+ for m in maps:
+ map = self.__read_mapfile(maps[m])
+ lines = []
+ cont = ''
+ for x in map:
+ if x.startswith('#'):
+ continue
+ x = x.rstrip()
+ if x.startswith('+'):
+ result['skipped'].append([m, maps[m]])
+ continue
+ if len(x) == 0:
+ continue
+ if x.endswith("\\"):
+ cont = cont + x[:-1] + ' '
+ else:
+ lines.append(cont + x)
+ cont=''
+ for x in lines:
+ am = x.split(None)
+ key = unicode(am[0].replace('"',''))
+ try:
+ api.Command['automountkey_add'](
+ args[0],
+ unicode(m),
+ automountkey=key,
+ automountinformation=unicode(' '.join(am[1:])))
+ result['keys'].append([key,m])
+ except errors.DuplicateEntry as e:
+ if options.get('continue', False):
+ result['duplicatekeys'].append(am[0])
+ else:
+ raise e
+
+ return dict(result=result)
+
+ def output_for_cli(self, textui, result, *keys, **options):
+ maps = result['result']['maps']
+ keys = result['result']['keys']
+ duplicatemaps = result['result']['duplicatemaps']
+ duplicatekeys = result['result']['duplicatekeys']
+ skipped = result['result']['skipped']
+
+ textui.print_plain('Imported maps:')
+ for m in maps:
+ textui.print_plain(
+ 'Added %s' % m
+ )
+ textui.print_plain('')
+
+ textui.print_plain('Imported keys:')
+ for k in keys:
+ textui.print_plain(
+ 'Added %s to %s' % (
+ k[0], k[1]
+ )
+ )
+ textui.print_plain('')
+
+ if len(skipped) > 0:
+ textui.print_plain('Ignored keys:')
+ for k in skipped:
+ textui.print_plain(
+ 'Ignored %s to %s' % (
+ k[0], k[1]
+ )
+ )
+
+
+ if options.get('continue', False) and len(duplicatemaps) > 0:
+ textui.print_plain('')
+ textui.print_plain('Duplicate maps skipped:')
+ for m in duplicatemaps:
+ textui.print_plain(
+ 'Skipped %s' % m
+ )
+
+
+ if options.get('continue', False) and len(duplicatekeys) > 0:
+ textui.print_plain('')
+ textui.print_plain('Duplicate keys skipped:')
+ for k in duplicatekeys:
+ textui.print_plain(
+ 'Skipped %s' % k
+ )
diff --git a/ipaclient/plugins/otptoken.py b/ipaclient/plugins/otptoken.py
new file mode 100644
index 000000000..66a457cde
--- /dev/null
+++ b/ipaclient/plugins/otptoken.py
@@ -0,0 +1,110 @@
+# Authors:
+# Nathaniel McCallum <npmccallum@redhat.com>
+#
+# Copyright (C) 2013 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program 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 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+from ipalib import api, Str, Password, _
+from ipalib.plugable import Registry
+from ipalib.frontend import Local
+from ipaplatform.paths import paths
+from ipapython.dn import DN
+from ipapython.nsslib import NSSConnection
+
+import six
+from six.moves import urllib
+
+if six.PY3:
+ unicode = str
+
+register = Registry()
+
+
+class HTTPSHandler(urllib.request.HTTPSHandler):
+ "Opens SSL HTTPS connections that perform hostname validation."
+
+ def __init__(self, **kwargs):
+ self.__kwargs = kwargs
+
+ # Can't use super() because the parent is an old-style class.
+ urllib.request.HTTPSHandler.__init__(self)
+
+ def __inner(self, host, **kwargs):
+ tmp = self.__kwargs.copy()
+ tmp.update(kwargs)
+ # NSSConnection doesn't support timeout argument
+ tmp.pop('timeout', None)
+ return NSSConnection(host, **tmp)
+
+ def https_open(self, req):
+ # pylint: disable=no-member
+ return self.do_open(self.__inner, req)
+
+@register()
+class otptoken_sync(Local):
+ __doc__ = _('Synchronize an OTP token.')
+
+ header = 'X-IPA-TokenSync-Result'
+
+ takes_options = (
+ Str('user', label=_('User ID')),
+ Password('password', label=_('Password'), confirm=False),
+ Password('first_code', label=_('First Code'), confirm=False),
+ Password('second_code', label=_('Second Code'), confirm=False),
+ )
+
+ takes_args = (
+ Str('token?', label=_('Token ID')),
+ )
+
+ def forward(self, *args, **kwargs):
+ status = {'result': {self.header: 'unknown'}}
+
+ # Get the sync URI.
+ segments = list(urllib.parse.urlparse(self.api.env.xmlrpc_uri))
+ assert segments[0] == 'https' # Ensure encryption.
+ segments[2] = segments[2].replace('/xml', '/session/sync_token')
+ # urlunparse *can* take one argument
+ # pylint: disable=too-many-function-args
+ sync_uri = urllib.parse.urlunparse(segments)
+
+ # Prepare the query.
+ query = {k: v for k, v in kwargs.items()
+ if k in {x.name for x in self.takes_options}}
+ if args and args[0] is not None:
+ obj = self.api.Object.otptoken
+ query['token'] = DN((obj.primary_key.name, args[0]),
+ obj.container_dn, self.api.env.basedn)
+ query = urllib.parse.urlencode(query)
+
+ # Sync the token.
+ # pylint: disable=E1101
+ handler = HTTPSHandler(dbdir=paths.IPA_NSSDB_DIR,
+ tls_version_min=api.env.tls_version_min,
+ tls_version_max=api.env.tls_version_max)
+ rsp = urllib.request.build_opener(handler).open(sync_uri, query)
+ if rsp.getcode() == 200:
+ status['result'][self.header] = rsp.info().get(self.header, 'unknown')
+ rsp.close()
+
+ return status
+
+ def output_for_cli(self, textui, result, *keys, **options):
+ textui.print_plain({
+ 'ok': 'Token synchronized.',
+ 'error': 'Error contacting server!',
+ 'invalid-credentials': 'Invalid Credentials!',
+ }.get(result['result'][self.header], 'Unknown Error!'))
diff --git a/ipaclient/plugins/otptoken_yubikey.py b/ipaclient/plugins/otptoken_yubikey.py
new file mode 100644
index 000000000..207d0a6e9
--- /dev/null
+++ b/ipaclient/plugins/otptoken_yubikey.py
@@ -0,0 +1,152 @@
+# Authors:
+# Nathaniel McCallum <npmccallum@redhat.com>
+#
+# Copyright (C) 2014 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program 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 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+import six
+import usb.core
+import yubico
+
+from ipalib import _, IntEnum
+from ipalib.errors import NotFound
+from ipalib.frontend import Command
+from ipalib.plugable import Registry
+
+if six.PY3:
+ unicode = str
+
+__doc__ = _("""
+YubiKey Tokens
+""") + _("""
+Manage YubiKey tokens.
+""") + _("""
+This code is an extension to the otptoken plugin and provides support for
+reading/writing YubiKey tokens directly.
+""") + _("""
+EXAMPLES:
+""") + _("""
+ Add a new token:
+ ipa otptoken-add-yubikey --owner=jdoe --desc="My YubiKey"
+""")
+
+register = Registry()
+
+topic = ('otp', _('One time password commands'))
+
+
+@register()
+class otptoken_add_yubikey(Command):
+ __doc__ = _('Add a new YubiKey OTP token.')
+
+ takes_options = (
+ IntEnum('slot?',
+ cli_name='slot',
+ label=_('YubiKey slot'),
+ values=(1, 2),
+ ),
+ )
+
+ def get_args(self):
+ for arg in self.api.Command.otptoken_add.args():
+ yield arg
+ for arg in super(otptoken_add_yubikey, self).get_args():
+ yield arg
+
+ def get_options(self):
+ for option in self.api.Command.otptoken_add.options():
+ if option.name not in ('type',
+ 'ipatokenvendor',
+ 'ipatokenmodel',
+ 'ipatokenserial',
+ 'ipatokenotpalgorithm',
+ 'ipatokenhotpcounter',
+ 'ipatokenotpkey',
+ 'ipatokentotpclockoffset',
+ 'ipatokentotptimestep',
+ 'no_qrcode',
+ 'qrcode',
+ 'version'):
+ yield option
+ for option in super(otptoken_add_yubikey, self).get_options():
+ yield option
+
+ def _iter_output(self):
+ return self.api.Command.otptoken_add.output()
+
+ def forward(self, *args, **kwargs):
+ # Open the YubiKey
+ try:
+ yk = yubico.find_yubikey()
+ except usb.core.USBError as e:
+ raise NotFound(reason="No YubiKey found: %s" % e.strerror)
+ except yubico.yubikey.YubiKeyError as e:
+ raise NotFound(reason=e.reason)
+
+ assert yk.version_num() >= (2, 1)
+
+ # If no slot is specified, find the first free slot.
+ if kwargs.get('slot', None) is None:
+ try:
+ used = yk.status().valid_configs()
+ kwargs['slot'] = sorted({1, 2}.difference(used))[0]
+ except IndexError:
+ raise NotFound(reason=_('No free YubiKey slot!'))
+
+ # Create the key (NOTE: the length is fixed).
+ key = os.urandom(20)
+
+ # Write the config.
+ cfg = yk.init_config()
+ cfg.mode_oath_hotp(key, kwargs['ipatokenotpdigits'])
+ cfg.extended_flag('SERIAL_API_VISIBLE', True)
+ yk.write_config(cfg, slot=kwargs['slot'])
+
+ # Filter the options we want to pass.
+ options = {k: v for k, v in kwargs.items() if k in (
+ 'version',
+ 'description',
+ 'ipatokenowner',
+ 'ipatokendisabled',
+ 'ipatokennotbefore',
+ 'ipatokennotafter',
+ 'ipatokenotpdigits',
+ )}
+
+ # Run the command.
+ answer = self.Backend.rpcclient.forward('otptoken_add',
+ *args,
+ type=u'hotp',
+ ipatokenvendor=u'YubiCo',
+ ipatokenmodel=unicode(yk.model),
+ ipatokenserial=unicode(yk.serial()),
+ ipatokenotpalgorithm=u'sha1',
+ ipatokenhotpcounter=0,
+ ipatokenotpkey=key,
+ no_qrcode=True,
+ **options)
+
+ # Suppress values we don't want to return.
+ for k in (u'uri', u'ipatokenotpkey'):
+ if k in answer.get('result', {}):
+ del answer['result'][k]
+
+ # Return which slot was used for writing.
+ answer.get('result', {})['slot'] = kwargs['slot']
+
+ return answer
diff --git a/ipaclient/plugins/rpcclient.py b/ipaclient/plugins/rpcclient.py
new file mode 100644
index 000000000..af2bc38d4
--- /dev/null
+++ b/ipaclient/plugins/rpcclient.py
@@ -0,0 +1,53 @@
+# Authors:
+# Jason Gerard DeRose <jderose@redhat.com>
+# Rob Crittenden <rcritten@redhat.com>
+# Petr Viktorin <pviktori@redhat.com>
+#
+# Copyright (C) 2008-2013 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program 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 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+"""
+RPC client plugins.
+"""
+
+from ipalib import Registry, api
+
+register = Registry()
+
+
+if 'in_server' in api.env and api.env.in_server is False:
+ from ipalib.rpc import xmlclient, jsonclient
+ register()(xmlclient)
+ register()(jsonclient)
+
+ # FIXME: api.register only looks at the class name, so we need to create
+ # trivial subclasses with the desired name.
+ if api.env.rpc_protocol == 'xmlrpc':
+
+ class rpcclient(xmlclient):
+ """xmlclient renamed to 'rpcclient'"""
+ pass
+ register()(rpcclient)
+
+ elif api.env.rpc_protocol == 'jsonrpc':
+
+ class rpcclient(jsonclient):
+ """jsonclient renamed to 'rpcclient'"""
+ pass
+ register()(rpcclient)
+
+ else:
+ raise ValueError('unknown rpc_protocol: %s' % api.env.rpc_protocol)
diff --git a/ipaclient/plugins/vault.py b/ipaclient/plugins/vault.py
new file mode 100644
index 000000000..ca132e8e6
--- /dev/null
+++ b/ipaclient/plugins/vault.py
@@ -0,0 +1,936 @@
+# Authors:
+# Endi S. Dewata <edewata@redhat.com>
+#
+# Copyright (C) 2015 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program 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 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import print_function
+
+import base64
+import getpass
+import io
+import json
+import os
+import sys
+
+from cryptography.fernet import Fernet, InvalidToken
+from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
+from cryptography.hazmat.primitives.asymmetric import padding
+from cryptography.hazmat.primitives.serialization import load_pem_public_key,\
+ load_pem_private_key
+
+import nss.nss as nss
+
+from ipalib.frontend import Local
+from ipalib import errors
+from ipalib import Bytes, Flag, Str
+from ipalib.plugable import Registry
+from ipalib import _
+from ipaplatform.paths import paths
+
+
+def validated_read(argname, filename, mode='r', encoding=None):
+ """Read file and catch errors
+
+ IOError and UnicodeError (for text files) are turned into a
+ ValidationError
+ """
+ try:
+ with io.open(filename, mode=mode, encoding=encoding) as f:
+ data = f.read()
+ except IOError as exc:
+ raise errors.ValidationError(
+ name=argname,
+ error=_("Cannot read file '%(filename)s': %(exc)s") % {
+ 'filename': filename, 'exc': exc.args[1]
+ }
+ )
+ except UnicodeError as exc:
+ raise errors.ValidationError(
+ name=argname,
+ error=_("Cannot decode file '%(filename)s': %(exc)s") % {
+ 'filename': filename, 'exc': exc
+ }
+ )
+ return data
+
+
+register = Registry()
+
+MAX_VAULT_DATA_SIZE = 2**20 # = 1 MB
+
+
+def get_new_password():
+ """
+ Gets new password from user and verify it.
+ """
+ while True:
+ password = getpass.getpass('New password: ').decode(
+ sys.stdin.encoding)
+ password2 = getpass.getpass('Verify password: ').decode(
+ sys.stdin.encoding)
+
+ if password == password2:
+ return password
+
+ print(' ** Passwords do not match! **')
+
+
+def get_existing_password():
+ """
+ Gets existing password from user.
+ """
+ return getpass.getpass('Password: ').decode(sys.stdin.encoding)
+
+
+def generate_symmetric_key(password, salt):
+ """
+ Generates symmetric key from password and salt.
+ """
+ kdf = PBKDF2HMAC(
+ algorithm=hashes.SHA256(),
+ length=32,
+ salt=salt,
+ iterations=100000,
+ backend=default_backend()
+ )
+
+ return base64.b64encode(kdf.derive(password.encode('utf-8')))
+
+
+def encrypt(data, symmetric_key=None, public_key=None):
+ """
+ Encrypts data with symmetric key or public key.
+ """
+ if symmetric_key:
+ fernet = Fernet(symmetric_key)
+ return fernet.encrypt(data)
+
+ elif public_key:
+ public_key_obj = load_pem_public_key(
+ data=public_key,
+ backend=default_backend()
+ )
+ return public_key_obj.encrypt(
+ data,
+ padding.OAEP(
+ mgf=padding.MGF1(algorithm=hashes.SHA1()),
+ algorithm=hashes.SHA1(),
+ label=None
+ )
+ )
+
+
+def decrypt(data, symmetric_key=None, private_key=None):
+ """
+ Decrypts data with symmetric key or public key.
+ """
+ if symmetric_key:
+ try:
+ fernet = Fernet(symmetric_key)
+ return fernet.decrypt(data)
+ except InvalidToken:
+ raise errors.AuthenticationError(
+ message=_('Invalid credentials'))
+
+ elif private_key:
+ try:
+ private_key_obj = load_pem_private_key(
+ data=private_key,
+ password=None,
+ backend=default_backend()
+ )
+ return private_key_obj.decrypt(
+ data,
+ padding.OAEP(
+ mgf=padding.MGF1(algorithm=hashes.SHA1()),
+ algorithm=hashes.SHA1(),
+ label=None
+ )
+ )
+ except AssertionError:
+ raise errors.AuthenticationError(
+ message=_('Invalid credentials'))
+
+
+@register()
+class vault_add(Local):
+ __doc__ = _('Create a new vault.')
+
+ takes_options = (
+ Str(
+ 'password?',
+ cli_name='password',
+ doc=_('Vault password'),
+ ),
+ Str( # TODO: use File parameter
+ 'password_file?',
+ cli_name='password_file',
+ doc=_('File containing the vault password'),
+ ),
+ Str( # TODO: use File parameter
+ 'public_key_file?',
+ cli_name='public_key_file',
+ doc=_('File containing the vault public key'),
+ ),
+ )
+
+ def get_args(self):
+ for arg in self.api.Command.vault_add_internal.args():
+ yield arg
+ for arg in super(vault_add, self).get_args():
+ yield arg
+
+ def get_options(self):
+ for option in self.api.Command.vault_add_internal.options():
+ if option.name not in ('ipavaultsalt', 'version'):
+ yield option
+ for option in super(vault_add, self).get_options():
+ yield option
+
+ def _iter_output(self):
+ return self.api.Command.vault_add_internal.output()
+
+ def forward(self, *args, **options):
+
+ vault_type = options.get('ipavaulttype')
+ password = options.get('password')
+ password_file = options.get('password_file')
+ public_key = options.get('ipavaultpublickey')
+ public_key_file = options.get('public_key_file')
+
+ # don't send these parameters to server
+ if 'password' in options:
+ del options['password']
+ if 'password_file' in options:
+ del options['password_file']
+ if 'public_key_file' in options:
+ del options['public_key_file']
+
+ if vault_type != u'symmetric' and (password or password_file):
+ raise errors.MutuallyExclusiveError(
+ reason=_('Password can be specified only for '
+ 'symmetric vault')
+ )
+
+ if vault_type != u'asymmetric' and (public_key or public_key_file):
+ raise errors.MutuallyExclusiveError(
+ reason=_('Public key can be specified only for '
+ 'asymmetric vault')
+ )
+
+ if self.api.env.in_server:
+ backend = self.api.Backend.ldap2
+ else:
+ backend = self.api.Backend.rpcclient
+ if not backend.isconnected():
+ backend.connect()
+
+ if vault_type == u'standard':
+
+ pass
+
+ elif vault_type == u'symmetric':
+
+ # get password
+ if password and password_file:
+ raise errors.MutuallyExclusiveError(
+ reason=_('Password specified multiple times'))
+
+ elif password:
+ pass
+
+ elif password_file:
+ password = validated_read('password-file',
+ password_file,
+ encoding='utf-8')
+ password = password.rstrip('\n')
+
+ else:
+ password = get_new_password()
+
+ # generate vault salt
+ options['ipavaultsalt'] = os.urandom(16)
+
+ elif vault_type == u'asymmetric':
+
+ # get new vault public key
+ if public_key and public_key_file:
+ raise errors.MutuallyExclusiveError(
+ reason=_('Public key specified multiple times'))
+
+ elif public_key:
+ pass
+
+ elif public_key_file:
+ public_key = validated_read('public-key-file',
+ public_key_file,
+ mode='rb')
+
+ # store vault public key
+ options['ipavaultpublickey'] = public_key
+
+ else:
+ raise errors.ValidationError(
+ name='ipavaultpublickey',
+ error=_('Missing vault public key'))
+
+ # validate public key and prevent users from accidentally
+ # sending a private key to the server.
+ try:
+ load_pem_public_key(
+ data=public_key,
+ backend=default_backend()
+ )
+ except ValueError as e:
+ raise errors.ValidationError(
+ name='ipavaultpublickey',
+ error=_('Invalid or unsupported vault public key: %s') % e,
+ )
+
+ # create vault
+ response = self.api.Command.vault_add_internal(*args, **options)
+
+ # prepare parameters for archival
+ opts = options.copy()
+ if 'description' in opts:
+ del opts['description']
+ if 'ipavaulttype' in opts:
+ del opts['ipavaulttype']
+
+ if vault_type == u'symmetric':
+ opts['password'] = password
+ del opts['ipavaultsalt']
+
+ elif vault_type == u'asymmetric':
+ del opts['ipavaultpublickey']
+
+ # archive blank data
+ self.api.Command.vault_archive(*args, **opts)
+
+ return response
+
+
+@register()
+class vault_mod(Local):
+ __doc__ = _('Modify a vault.')
+
+ takes_options = (
+ Flag(
+ 'change_password?',
+ doc=_('Change password'),
+ ),
+ Str(
+ 'old_password?',
+ cli_name='old_password',
+ doc=_('Old vault password'),
+ ),
+ Str( # TODO: use File parameter
+ 'old_password_file?',
+ cli_name='old_password_file',
+ doc=_('File containing the old vault password'),
+ ),
+ Str(
+ 'new_password?',
+ cli_name='new_password',
+ doc=_('New vault password'),
+ ),
+ Str( # TODO: use File parameter
+ 'new_password_file?',
+ cli_name='new_password_file',
+ doc=_('File containing the new vault password'),
+ ),
+ Bytes(
+ 'private_key?',
+ cli_name='private_key',
+ doc=_('Old vault private key'),
+ ),
+ Str( # TODO: use File parameter
+ 'private_key_file?',
+ cli_name='private_key_file',
+ doc=_('File containing the old vault private key'),
+ ),
+ Str( # TODO: use File parameter
+ 'public_key_file?',
+ cli_name='public_key_file',
+ doc=_('File containing the new vault public key'),
+ ),
+ )
+
+ def get_args(self):
+ for arg in self.api.Command.vault_mod_internal.args():
+ yield arg
+ for arg in super(vault_mod, self).get_args():
+ yield arg
+
+ def get_options(self):
+ for option in self.api.Command.vault_mod_internal.options():
+ if option.name not in ('ipavaultsalt', 'version'):
+ yield option
+ for option in super(vault_mod, self).get_options():
+ yield option
+
+ def _iter_output(self):
+ return self.api.Command.vault_mod_internal.output()
+
+ def forward(self, *args, **options):
+
+ vault_type = options.pop('ipavaulttype', False)
+ salt = options.pop('ipavaultsalt', False)
+ change_password = options.pop('change_password', False)
+
+ old_password = options.pop('old_password', None)
+ old_password_file = options.pop('old_password_file', None)
+ new_password = options.pop('new_password', None)
+ new_password_file = options.pop('new_password_file', None)
+
+ old_private_key = options.pop('private_key', None)
+ old_private_key_file = options.pop('private_key_file', None)
+ new_public_key = options.pop('ipavaultpublickey', None)
+ new_public_key_file = options.pop('public_key_file', None)
+
+ if self.api.env.in_server:
+ backend = self.api.Backend.ldap2
+ else:
+ backend = self.api.Backend.rpcclient
+ if not backend.isconnected():
+ backend.connect()
+
+ # determine the vault type based on parameters specified
+ if vault_type:
+ pass
+
+ elif change_password or new_password or new_password_file or salt:
+ vault_type = u'symmetric'
+
+ elif new_public_key or new_public_key_file:
+ vault_type = u'asymmetric'
+
+ # if vault type is specified, retrieve existing secret
+ if vault_type:
+ opts = options.copy()
+ opts.pop('description', None)
+
+ opts['password'] = old_password
+ opts['password_file'] = old_password_file
+ opts['private_key'] = old_private_key
+ opts['private_key_file'] = old_private_key_file
+
+ response = self.api.Command.vault_retrieve(*args, **opts)
+ data = response['result']['data']
+
+ opts = options.copy()
+
+ # if vault type is specified, update crypto attributes
+ if vault_type:
+ opts['ipavaulttype'] = vault_type
+
+ if vault_type == u'standard':
+ opts['ipavaultsalt'] = None
+ opts['ipavaultpublickey'] = None
+
+ elif vault_type == u'symmetric':
+ if salt:
+ opts['ipavaultsalt'] = salt
+ else:
+ opts['ipavaultsalt'] = os.urandom(16)
+
+ opts['ipavaultpublickey'] = None
+
+ elif vault_type == u'asymmetric':
+
+ # get new vault public key
+ if new_public_key and new_public_key_file:
+ raise errors.MutuallyExclusiveError(
+ reason=_('New public key specified multiple times'))
+
+ elif new_public_key:
+ pass
+
+ elif new_public_key_file:
+ new_public_key = validated_read('public_key_file',
+ new_public_key_file,
+ mode='rb')
+
+ else:
+ raise errors.ValidationError(
+ name='ipavaultpublickey',
+ error=_('Missing new vault public key'))
+
+ opts['ipavaultsalt'] = None
+ opts['ipavaultpublickey'] = new_public_key
+
+ response = self.api.Command.vault_mod_internal(*args, **opts)
+
+ # if vault type is specified, rearchive existing secret
+ if vault_type:
+ opts = options.copy()
+ opts.pop('description', None)
+
+ opts['data'] = data
+ opts['password'] = new_password
+ opts['password_file'] = new_password_file
+ opts['override_password'] = True
+
+ self.api.Command.vault_archive(*args, **opts)
+
+ return response
+
+
+@register()
+class vault_archive(Local):
+ __doc__ = _('Archive data into a vault.')
+
+ takes_options = (
+ Bytes(
+ 'data?',
+ doc=_('Binary data to archive'),
+ ),
+ Str( # TODO: use File parameter
+ 'in?',
+ doc=_('File containing data to archive'),
+ ),
+ Str(
+ 'password?',
+ cli_name='password',
+ doc=_('Vault password'),
+ ),
+ Str( # TODO: use File parameter
+ 'password_file?',
+ cli_name='password_file',
+ doc=_('File containing the vault password'),
+ ),
+ Flag(
+ 'override_password?',
+ doc=_('Override existing password'),
+ ),
+ )
+
+ def get_args(self):
+ for arg in self.api.Command.vault_archive_internal.args():
+ yield arg
+ for arg in super(vault_archive, self).get_args():
+ yield arg
+
+ def get_options(self):
+ for option in self.api.Command.vault_archive_internal.options():
+ if option.name not in ('nonce',
+ 'session_key',
+ 'vault_data',
+ 'version'):
+ yield option
+ for option in super(vault_archive, self).get_options():
+ yield option
+
+ def _iter_output(self):
+ return self.api.Command.vault_archive_internal.output()
+
+ def forward(self, *args, **options):
+
+ name = args[-1]
+
+ data = options.get('data')
+ input_file = options.get('in')
+
+ password = options.get('password')
+ password_file = options.get('password_file')
+
+ override_password = options.pop('override_password', False)
+
+ # don't send these parameters to server
+ if 'data' in options:
+ del options['data']
+ if 'in' in options:
+ del options['in']
+ if 'password' in options:
+ del options['password']
+ if 'password_file' in options:
+ del options['password_file']
+
+ # get data
+ if data and input_file:
+ raise errors.MutuallyExclusiveError(
+ reason=_('Input data specified multiple times'))
+
+ elif data:
+ if len(data) > MAX_VAULT_DATA_SIZE:
+ raise errors.ValidationError(name="data", error=_(
+ "Size of data exceeds the limit. Current vault data size "
+ "limit is %(limit)d B")
+ % {'limit': MAX_VAULT_DATA_SIZE})
+
+ elif input_file:
+ try:
+ stat = os.stat(input_file)
+ except OSError as exc:
+ raise errors.ValidationError(name="in", error=_(
+ "Cannot read file '%(filename)s': %(exc)s")
+ % {'filename': input_file, 'exc': exc.args[1]})
+ if stat.st_size > MAX_VAULT_DATA_SIZE:
+ raise errors.ValidationError(name="in", error=_(
+ "Size of data exceeds the limit. Current vault data size "
+ "limit is %(limit)d B")
+ % {'limit': MAX_VAULT_DATA_SIZE})
+ data = validated_read('in', input_file, mode='rb')
+
+ else:
+ data = ''
+
+ if self.api.env.in_server:
+ backend = self.api.Backend.ldap2
+ else:
+ backend = self.api.Backend.rpcclient
+ if not backend.isconnected():
+ backend.connect()
+
+ # retrieve vault info
+ vault = self.api.Command.vault_show(*args, **options)['result']
+
+ vault_type = vault['ipavaulttype'][0]
+
+ if vault_type == u'standard':
+
+ encrypted_key = None
+
+ elif vault_type == u'symmetric':
+
+ # get password
+ if password and password_file:
+ raise errors.MutuallyExclusiveError(
+ reason=_('Password specified multiple times'))
+
+ elif password:
+ pass
+
+ elif password_file:
+ password = validated_read('password-file',
+ password_file,
+ encoding='utf-8')
+ password = password.rstrip('\n')
+
+ else:
+ if override_password:
+ password = get_new_password()
+ else:
+ password = get_existing_password()
+
+ if not override_password:
+ # verify password by retrieving existing data
+ opts = options.copy()
+ opts['password'] = password
+ try:
+ self.api.Command.vault_retrieve(*args, **opts)
+ except errors.NotFound:
+ pass
+
+ salt = vault['ipavaultsalt'][0]
+
+ # generate encryption key from vault password
+ encryption_key = generate_symmetric_key(password, salt)
+
+ # encrypt data with encryption key
+ data = encrypt(data, symmetric_key=encryption_key)
+
+ encrypted_key = None
+
+ elif vault_type == u'asymmetric':
+
+ public_key = vault['ipavaultpublickey'][0].encode('utf-8')
+
+ # generate encryption key
+ encryption_key = base64.b64encode(os.urandom(32))
+
+ # encrypt data with encryption key
+ data = encrypt(data, symmetric_key=encryption_key)
+
+ # encrypt encryption key with public key
+ encrypted_key = encrypt(encryption_key, public_key=public_key)
+
+ else:
+ raise errors.ValidationError(
+ name='vault_type',
+ error=_('Invalid vault type'))
+
+ # initialize NSS database
+ current_dbdir = paths.IPA_NSSDB_DIR
+ nss.nss_init(current_dbdir)
+
+ # retrieve transport certificate
+ config = self.api.Command.vaultconfig_show()['result']
+ transport_cert_der = config['transport_cert']
+ nss_transport_cert = nss.Certificate(transport_cert_der)
+
+ # generate session key
+ mechanism = nss.CKM_DES3_CBC_PAD
+ slot = nss.get_best_slot(mechanism)
+ key_length = slot.get_best_key_length(mechanism)
+ session_key = slot.key_gen(mechanism, None, key_length)
+
+ # wrap session key with transport certificate
+ # pylint: disable=no-member
+ public_key = nss_transport_cert.subject_public_key_info.public_key
+ # pylint: enable=no-member
+ wrapped_session_key = nss.pub_wrap_sym_key(mechanism,
+ public_key,
+ session_key)
+
+ options['session_key'] = wrapped_session_key.data
+
+ nonce_length = nss.get_iv_length(mechanism)
+ nonce = nss.generate_random(nonce_length)
+ options['nonce'] = nonce
+
+ vault_data = {}
+ vault_data[u'data'] = base64.b64encode(data).decode('utf-8')
+
+ if encrypted_key:
+ vault_data[u'encrypted_key'] = base64.b64encode(encrypted_key)\
+ .decode('utf-8')
+
+ json_vault_data = json.dumps(vault_data)
+
+ # wrap vault_data with session key
+ iv_si = nss.SecItem(nonce)
+ iv_param = nss.param_from_iv(mechanism, iv_si)
+
+ encoding_ctx = nss.create_context_by_sym_key(mechanism,
+ nss.CKA_ENCRYPT,
+ session_key,
+ iv_param)
+
+ wrapped_vault_data = encoding_ctx.cipher_op(json_vault_data)\
+ + encoding_ctx.digest_final()
+
+ options['vault_data'] = wrapped_vault_data
+
+ return self.api.Command.vault_archive_internal(*args, **options)
+
+
+@register()
+class vault_retrieve(Local):
+ __doc__ = _('Retrieve a data from a vault.')
+
+ takes_options = (
+ Str(
+ 'out?',
+ doc=_('File to store retrieved data'),
+ ),
+ Str(
+ 'password?',
+ cli_name='password',
+ doc=_('Vault password'),
+ ),
+ Str( # TODO: use File parameter
+ 'password_file?',
+ cli_name='password_file',
+ doc=_('File containing the vault password'),
+ ),
+ Bytes(
+ 'private_key?',
+ cli_name='private_key',
+ doc=_('Vault private key'),
+ ),
+ Str( # TODO: use File parameter
+ 'private_key_file?',
+ cli_name='private_key_file',
+ doc=_('File containing the vault private key'),
+ ),
+ )
+
+ has_output_params = (
+ Bytes(
+ 'data',
+ label=_('Data'),
+ ),
+ )
+
+ def get_args(self):
+ for arg in self.api.Command.vault_retrieve_internal.args():
+ yield arg
+ for arg in super(vault_retrieve, self).get_args():
+ yield arg
+
+ def get_options(self):
+ for option in self.api.Command.vault_retrieve_internal.options():
+ if option.name not in ('session_key', 'version'):
+ yield option
+ for option in super(vault_retrieve, self).get_options():
+ yield option
+
+ def _iter_output(self):
+ return self.api.Command.vault_retrieve_internal.output()
+
+ def forward(self, *args, **options):
+
+ name = args[-1]
+
+ output_file = options.get('out')
+
+ password = options.get('password')
+ password_file = options.get('password_file')
+ private_key = options.get('private_key')
+ private_key_file = options.get('private_key_file')
+
+ # don't send these parameters to server
+ if 'out' in options:
+ del options['out']
+ if 'password' in options:
+ del options['password']
+ if 'password_file' in options:
+ del options['password_file']
+ if 'private_key' in options:
+ del options['private_key']
+ if 'private_key_file' in options:
+ del options['private_key_file']
+
+ if self.api.env.in_server:
+ backend = self.api.Backend.ldap2
+ else:
+ backend = self.api.Backend.rpcclient
+ if not backend.isconnected():
+ backend.connect()
+
+ # retrieve vault info
+ vault = self.api.Command.vault_show(*args, **options)['result']
+
+ vault_type = vault['ipavaulttype'][0]
+
+ # initialize NSS database
+ current_dbdir = paths.IPA_NSSDB_DIR
+ nss.nss_init(current_dbdir)
+
+ # retrieve transport certificate
+ config = self.api.Command.vaultconfig_show()['result']
+ transport_cert_der = config['transport_cert']
+ nss_transport_cert = nss.Certificate(transport_cert_der)
+
+ # generate session key
+ mechanism = nss.CKM_DES3_CBC_PAD
+ slot = nss.get_best_slot(mechanism)
+ key_length = slot.get_best_key_length(mechanism)
+ session_key = slot.key_gen(mechanism, None, key_length)
+
+ # wrap session key with transport certificate
+ # pylint: disable=no-member
+ public_key = nss_transport_cert.subject_public_key_info.public_key
+ # pylint: enable=no-member
+ wrapped_session_key = nss.pub_wrap_sym_key(mechanism,
+ public_key,
+ session_key)
+
+ # send retrieval request to server
+ options['session_key'] = wrapped_session_key.data
+
+ response = self.api.Command.vault_retrieve_internal(*args, **options)
+
+ result = response['result']
+ nonce = result['nonce']
+
+ # unwrap data with session key
+ wrapped_vault_data = result['vault_data']
+
+ iv_si = nss.SecItem(nonce)
+ iv_param = nss.param_from_iv(mechanism, iv_si)
+
+ decoding_ctx = nss.create_context_by_sym_key(mechanism,
+ nss.CKA_DECRYPT,
+ session_key,
+ iv_param)
+
+ json_vault_data = decoding_ctx.cipher_op(wrapped_vault_data)\
+ + decoding_ctx.digest_final()
+
+ vault_data = json.loads(json_vault_data)
+ data = base64.b64decode(vault_data[u'data'].encode('utf-8'))
+
+ encrypted_key = None
+
+ if 'encrypted_key' in vault_data:
+ encrypted_key = base64.b64decode(vault_data[u'encrypted_key']
+ .encode('utf-8'))
+
+ if vault_type == u'standard':
+
+ pass
+
+ elif vault_type == u'symmetric':
+
+ salt = vault['ipavaultsalt'][0]
+
+ # get encryption key from vault password
+ if password and password_file:
+ raise errors.MutuallyExclusiveError(
+ reason=_('Password specified multiple times'))
+
+ elif password:
+ pass
+
+ elif password_file:
+ password = validated_read('password-file',
+ password_file,
+ encoding='utf-8')
+ password = password.rstrip('\n')
+
+ else:
+ password = get_existing_password()
+
+ # generate encryption key from password
+ encryption_key = generate_symmetric_key(password, salt)
+
+ # decrypt data with encryption key
+ data = decrypt(data, symmetric_key=encryption_key)
+
+ elif vault_type == u'asymmetric':
+
+ # get encryption key with vault private key
+ if private_key and private_key_file:
+ raise errors.MutuallyExclusiveError(
+ reason=_('Private key specified multiple times'))
+
+ elif private_key:
+ pass
+
+ elif private_key_file:
+ private_key = validated_read('private-key-file',
+ private_key_file,
+ mode='rb')
+
+ else:
+ raise errors.ValidationError(
+ name='private_key',
+ error=_('Missing vault private key'))
+
+ # decrypt encryption key with private key
+ encryption_key = decrypt(encrypted_key, private_key=private_key)
+
+ # decrypt data with encryption key
+ data = decrypt(data, symmetric_key=encryption_key)
+
+ else:
+ raise errors.ValidationError(
+ name='vault_type',
+ error=_('Invalid vault type'))
+
+ if output_file:
+ with open(output_file, 'w') as f:
+ f.write(data)
+
+ else:
+ response['result'] = {'data': data}
+
+ return response