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>
This commit is contained in:
Jan Cholasta
2016-04-28 09:46:03 +02:00
parent f1ad3e67ae
commit 327d95296a
8 changed files with 1276 additions and 1185 deletions

View File

@@ -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
)

View File

@@ -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!'))

936
ipaclient/plugins/vault.py Normal file
View File

@@ -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

View File

@@ -18,13 +18,10 @@
# 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, IA5Str
from ipalib.frontend import Command
from ipalib import Str, IA5Str
from ipalib.plugable import Registry
from .baseldap import (
pkey_to_value,
@@ -211,8 +208,6 @@ automountInformation: -ro,soft,rsize=8192,wsize=8192 nfs.example.com:/vol/arch
register = Registry()
DIRECT_MAP_KEY = u'/-'
DEFAULT_MAPS = (u'auto.direct', )
DEFAULT_KEYS = (u'/-', )
@register()
class automountlocation(LDAPObject):
@@ -392,196 +387,6 @@ class automountlocation_tofiles(LDAPQuery):
)
@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
)
@register()
class automountmap(LDAPObject):
"""

View File

@@ -22,7 +22,7 @@ import sys
from .baseldap import LDAPObject, LDAPAddMember, LDAPRemoveMember
from .baseldap import LDAPCreate, LDAPDelete, LDAPUpdate, LDAPSearch, LDAPRetrieve
from ipalib import api, Int, Str, Bool, DateTime, Flag, Bytes, IntEnum, StrEnum, Password, _, ngettext
from ipalib import api, Int, Str, Bool, DateTime, Flag, Bytes, IntEnum, StrEnum, _, ngettext
from ipalib.messages import add_message, ResultFormattingError
from ipalib.plugable import Registry
from ipalib.errors import (
@@ -31,10 +31,7 @@ from ipalib.errors import (
NotFound,
ValidationError)
from ipalib.request import context
from ipalib.frontend import Local
from ipaplatform.paths import paths
from ipapython.dn import DN
from ipapython.nsslib import NSSConnection
from ipapython.version import API_VERSION
import base64
@@ -542,80 +539,3 @@ class otptoken_remove_managedby(LDAPRemoveMember):
__doc__ = _('Remove users that can manage this token.')
member_attributes = ['managedby']
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!'))

View File

@@ -19,24 +19,7 @@
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 Command, Object, Local
from ipalib.frontend import Command, Object
from ipalib import api, errors
from ipalib import Bytes, Flag, Str, StrEnum
from ipalib import output
@@ -49,7 +32,6 @@ from ipalib.request import context
from .baseuser import split_principal
from .service import normalize_principal
from ipalib import _, ngettext
from ipaplatform.paths import paths
from ipapython.dn import DN
if api.env.in_server:
@@ -208,36 +190,8 @@ EXAMPLES:
""")
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
vault_options = (
Str(
'service?',
@@ -801,257 +755,6 @@ class vault(LDAPObject):
entry['username'] = entry.dn[1]['cn']
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_add_internal(LDAPCreate):
@@ -1202,172 +905,6 @@ class vault_find(LDAPSearch):
raise exc
@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_mod_internal(LDAPUpdate):
@@ -1470,235 +1007,6 @@ class vaultconfig_show(Retrieve):
}
@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_archive_internal(PKQuery):
@@ -1776,221 +1084,6 @@ class vault_archive_internal(PKQuery):
return response
@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
@register()
class vault_retrieve_internal(PKQuery):