• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import dbus, gobject, logging, os, random, re, shutil, string, sys, time
6from dbus.mainloop.glib import DBusGMainLoop
7
8import common, constants
9from autotest_lib.client.bin import utils
10from autotest_lib.client.common_lib import error
11from autotest_lib.client.cros.cros_disks import DBusClient
12
13CRYPTOHOME_CMD = '/usr/sbin/cryptohome'
14GUEST_USER_NAME = '$guest'
15UNAVAILABLE_ACTION = 'Unknown action or no action given.'
16MOUNT_RETRY_COUNT = 20
17TEMP_MOUNT_PATTERN = '/home/.shadow/%s/temporary_mount'
18VAULT_PATH_PATTERN = '/home/.shadow/%s/vault'
19
20DBUS_PROTOS_DEP = 'dbus_protos'
21
22
23class ChromiumOSError(error.TestError):
24    """Generic error for ChromiumOS-specific exceptions."""
25    pass
26
27def __run_cmd(cmd):
28    return utils.system_output(cmd + ' 2>&1', retain_output=True,
29                               ignore_status=True).strip()
30
31def get_user_hash(user):
32    """Get the user hash for the given user."""
33    return utils.system_output(['cryptohome', '--action=obfuscate_user',
34                                '--user=%s' % user])
35
36
37def user_path(user):
38    """Get the user mount point for the given user."""
39    return utils.system_output(['cryptohome-path', 'user', user])
40
41
42def system_path(user):
43    """Get the system mount point for the given user."""
44    return utils.system_output(['cryptohome-path', 'system', user])
45
46
47def temporary_mount_path(user):
48    """Get the vault mount path used during crypto-migration for the user.
49
50    @param user: user the temporary mount should be for
51    """
52    return TEMP_MOUNT_PATTERN % (get_user_hash(user))
53
54
55def vault_path(user):
56    """ Get the vault path for the given user.
57
58    @param user: The user who's vault path should be returned.
59    """
60    return VAULT_PATH_PATTERN % (get_user_hash(user))
61
62
63def ensure_clean_cryptohome_for(user, password=None):
64    """Ensure a fresh cryptohome exists for user.
65
66    @param user: user who needs a shiny new cryptohome.
67    @param password: if unset, a random password will be used.
68    """
69    if not password:
70        password = ''.join(random.sample(string.ascii_lowercase, 6))
71    unmount_vault(user)
72    remove_vault(user)
73    mount_vault(user, password, create=True)
74
75
76def get_tpm_status():
77    """Get the TPM status.
78
79    Returns:
80        A TPM status dictionary, for example:
81        { 'Enabled': True,
82          'Owned': True,
83          'Being Owned': False,
84          'Ready': True,
85          'Password': ''
86        }
87    """
88    out = __run_cmd(CRYPTOHOME_CMD + ' --action=tpm_status')
89    status = {}
90    for field in ['Enabled', 'Owned', 'Being Owned', 'Ready']:
91        match = re.search('TPM %s: (true|false)' % field, out)
92        if not match:
93            raise ChromiumOSError('Invalid TPM status: "%s".' % out)
94        status[field] = match.group(1) == 'true'
95    match = re.search('TPM Password: (\w*)', out)
96    status['Password'] = ''
97    if match:
98        status['Password'] = match.group(1)
99    return status
100
101
102def get_tpm_more_status():
103    """Get more of the TPM status.
104
105    Returns:
106        A TPM more status dictionary, for example:
107        { 'dictionary_attack_lockout_in_effect': False,
108          'attestation_prepared': False,
109          'boot_lockbox_finalized': False,
110          'enabled': True,
111          'owned': True,
112          'owner_password': ''
113          'dictionary_attack_counter': 0,
114          'dictionary_attack_lockout_seconds_remaining': 0,
115          'dictionary_attack_threshold': 10,
116          'attestation_enrolled': False,
117          'initialized': True,
118          'verified_boot_measured': False,
119          'install_lockbox_finalized': True
120        }
121        An empty dictionary is returned if the command is not supported.
122    """
123    status = {}
124    out = __run_cmd(CRYPTOHOME_CMD + ' --action=tpm_more_status | grep :')
125    if out.startswith(UNAVAILABLE_ACTION):
126        # --action=tpm_more_status only exists >= 41.
127        logging.info('Method not supported!')
128        return status
129    for line in out.splitlines():
130        items = line.strip().split(':')
131        if items[1].strip() == 'false':
132            value = False
133        elif items[1].strip() == 'true':
134            value = True
135        elif items[1].strip().isdigit():
136            value = int(items[1].strip())
137        else:
138            value = items[1].strip(' "')
139        status[items[0]] = value
140    return status
141
142
143def get_fwmp(cleared_fwmp=False):
144    """Get the firmware management parameters.
145
146    Args:
147        cleared_fwmp: True if the space should not exist.
148
149    Returns:
150        The dictionary with the FWMP contents, for example:
151        { 'flags': 0xbb41,
152          'developer_key_hash':
153            "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\
154             000\000\000\000\000\000\000\000\000\000\000",
155        }
156        or a dictionary with the Error if the FWMP doesn't exist and
157        cleared_fwmp is True
158        { 'error': 'CRYPTOHOME_ERROR_FIRMWARE_MANAGEMENT_PARAMETERS_INVALID' }
159
160    Raises:
161         ChromiumOSError if any expected field is not found in the cryptohome
162         output. This would typically happen when FWMP state does not match
163         'clreared_fwmp'
164    """
165    out = __run_cmd(CRYPTOHOME_CMD +
166                    ' --action=get_firmware_management_parameters')
167
168    if cleared_fwmp:
169        fields = ['error']
170    else:
171        fields = ['flags', 'developer_key_hash']
172
173    status = {}
174    for field in fields:
175        match = re.search('%s: (\S+)\n' % field, out)
176        if not match:
177            raise ChromiumOSError('Invalid FWMP field %s: "%s".' %
178                                  (field, out))
179        status[field] = match.group(1)
180    return status
181
182
183def set_fwmp(flags, developer_key_hash=None):
184    """Set the firmware management parameter contents.
185
186    Args:
187        developer_key_hash: a string with the developer key hash
188
189    Raises:
190         ChromiumOSError cryptohome cannot set the FWMP contents
191    """
192    cmd = (CRYPTOHOME_CMD +
193          ' --action=set_firmware_management_parameters '
194          '--flags=' + flags)
195    if developer_key_hash:
196        cmd += ' --developer_key_hash=' + developer_key_hash
197
198    out = __run_cmd(cmd)
199    if 'SetFirmwareManagementParameters success' not in out:
200        raise ChromiumOSError('failed to set FWMP: %s' % out)
201
202
203def is_tpm_lockout_in_effect():
204    """Returns true if the TPM lockout is in effect; false otherwise."""
205    status = get_tpm_more_status()
206    return status.get('dictionary_attack_lockout_in_effect', None)
207
208
209def get_login_status():
210    """Query the login status
211
212    Returns:
213        A login status dictionary containing:
214        { 'owner_user_exists': True|False,
215          'boot_lockbox_finalized': True|False
216        }
217    """
218    out = __run_cmd(CRYPTOHOME_CMD + ' --action=get_login_status')
219    status = {}
220    for field in ['owner_user_exists', 'boot_lockbox_finalized']:
221        match = re.search('%s: (true|false)' % field, out)
222        if not match:
223            raise ChromiumOSError('Invalid login status: "%s".' % out)
224        status[field] = match.group(1) == 'true'
225    return status
226
227
228def get_tpm_attestation_status():
229    """Get the TPM attestation status.  Works similar to get_tpm_status().
230    """
231    out = __run_cmd(CRYPTOHOME_CMD + ' --action=tpm_attestation_status')
232    status = {}
233    for field in ['Prepared', 'Enrolled']:
234        match = re.search('Attestation %s: (true|false)' % field, out)
235        if not match:
236            raise ChromiumOSError('Invalid attestation status: "%s".' % out)
237        status[field] = match.group(1) == 'true'
238    return status
239
240
241def take_tpm_ownership(wait_for_ownership=True):
242    """Take TPM owernship.
243
244    Args:
245        wait_for_ownership: block until TPM is owned if true
246    """
247    __run_cmd(CRYPTOHOME_CMD + ' --action=tpm_take_ownership')
248    if wait_for_ownership:
249        # Note that waiting for the 'Ready' flag is more correct than waiting
250        # for the 'Owned' flag, as the latter is set by cryptohomed before some
251        # of the ownership tasks are completed.
252        utils.poll_for_condition(
253                lambda: get_tpm_status()['Ready'],
254                timeout=300,
255                exception=error.TestError('Timeout waiting for TPM ownership'))
256
257
258def verify_ek():
259    """Verify the TPM endorsement key.
260
261    Returns true if EK is valid.
262    """
263    cmd = CRYPTOHOME_CMD + ' --action=tpm_verify_ek'
264    return (utils.system(cmd, ignore_status=True) == 0)
265
266
267def remove_vault(user):
268    """Remove the given user's vault from the shadow directory."""
269    logging.debug('user is %s', user)
270    user_hash = get_user_hash(user)
271    logging.debug('Removing vault for user %s with hash %s', user, user_hash)
272    cmd = CRYPTOHOME_CMD + ' --action=remove --force --user=%s' % user
273    __run_cmd(cmd)
274    # Ensure that the vault does not exist.
275    if os.path.exists(os.path.join(constants.SHADOW_ROOT, user_hash)):
276        raise ChromiumOSError('Cryptohome could not remove the user\'s vault.')
277
278
279def remove_all_vaults():
280    """Remove any existing vaults from the shadow directory.
281
282    This function must be run with root privileges.
283    """
284    for item in os.listdir(constants.SHADOW_ROOT):
285        abs_item = os.path.join(constants.SHADOW_ROOT, item)
286        if os.path.isdir(os.path.join(abs_item, 'vault')):
287            logging.debug('Removing vault for user with hash %s', item)
288            shutil.rmtree(abs_item)
289
290
291def mount_vault(user, password, create=False, key_label=None):
292    """Mount the given user's vault. Mounts should be created by calling this
293    function with create=True, and can be used afterwards with create=False.
294    Only try to mount existing vaults created with this function.
295
296    """
297    args = [CRYPTOHOME_CMD, '--action=mount_ex', '--user=%s' % user,
298            '--password=%s' % password, '--async']
299    if create:
300        args += ['--create']
301        if key_label is None:
302            key_label = 'bar'
303    if key_label is not None:
304        args += ['--key_label=%s' % key_label]
305    logging.info(__run_cmd(' '.join(args)))
306    # Ensure that the vault exists in the shadow directory.
307    user_hash = get_user_hash(user)
308    if not os.path.exists(os.path.join(constants.SHADOW_ROOT, user_hash)):
309        retry = 0
310        mounted = False
311        while retry < MOUNT_RETRY_COUNT and not mounted:
312            time.sleep(1)
313            logging.info("Retry " + str(retry + 1))
314            __run_cmd(' '.join(args))
315            # TODO: Remove this additional call to get_user_hash(user) when
316            # crbug.com/690994 is fixed
317            user_hash = get_user_hash(user)
318            if os.path.exists(os.path.join(constants.SHADOW_ROOT, user_hash)):
319                mounted = True
320            retry += 1
321        if not mounted:
322            raise ChromiumOSError('Cryptohome vault not found after mount.')
323    # Ensure that the vault is mounted.
324    if not is_permanent_vault_mounted(user=user, allow_fail=True):
325        raise ChromiumOSError('Cryptohome created a vault but did not mount.')
326
327
328def mount_guest():
329    """Mount the guest vault."""
330    args = [CRYPTOHOME_CMD, '--action=mount_guest_ex']
331    logging.info(__run_cmd(' '.join(args)))
332    # Ensure that the guest vault is mounted.
333    if not is_guest_vault_mounted(allow_fail=True):
334        raise ChromiumOSError('Cryptohome did not mount guest vault.')
335
336
337def test_auth(user, password):
338    cmd = [CRYPTOHOME_CMD, '--action=check_key_ex', '--user=%s' % user,
339           '--password=%s' % password, '--async']
340    out = __run_cmd(' '.join(cmd))
341    logging.info(out)
342    return 'Key authenticated.' in out
343
344
345def add_le_key(user, password, new_password, new_key_label):
346    args = [CRYPTOHOME_CMD, '--action=add_key_ex', '--key_policy=le',
347            '--user=%s' % user, '--password=%s' % password,
348            '--new_key_label=%s' % new_key_label,
349            '--new_password=%s' % new_password]
350    logging.info(__run_cmd(' '.join(args)))
351
352
353def remove_key(user, password, remove_key_label):
354    args = [CRYPTOHOME_CMD, '--action=remove_key_ex', '--user=%s' % user,
355            '--password=%s' % password,
356            '--remove_key_label=%s' % remove_key_label]
357    logging.info(__run_cmd(' '.join(args)))
358
359
360def get_supported_key_policies():
361    args = [CRYPTOHOME_CMD, '--action=get_supported_key_policies']
362    out = __run_cmd(' '.join(args))
363    logging.info(out)
364    policies = {}
365    for line in out.splitlines():
366        match = re.search('  ([^:]+): (true|false)', line)
367        if match:
368            policies[match.group(1)] = match.group(2) == 'true'
369    return policies
370
371
372def unmount_vault(user=None):
373    """Unmount the given user's vault.
374
375    Once unmounting for a specific user is supported, the user parameter will
376    name the target user. See crosbug.com/20778.
377    """
378    __run_cmd(CRYPTOHOME_CMD + ' --action=unmount')
379    # Ensure that the vault is not mounted.
380    if user is not None and is_vault_mounted(user, allow_fail=True):
381        raise ChromiumOSError('Cryptohome did not unmount the user.')
382
383
384def __get_mount_info(mount_point, allow_fail=False):
385    """Get information about the active mount at a given mount point."""
386    cryptohomed_path = '/proc/$(pgrep cryptohomed)/mounts'
387    try:
388        logging.debug("Active cryptohome mounts:\n" +
389                      utils.system_output('cat %s' % cryptohomed_path))
390        mount_line = utils.system_output(
391            'grep %s %s' % (mount_point, cryptohomed_path),
392            ignore_status=allow_fail)
393    except Exception as e:
394        logging.error(e)
395        raise ChromiumOSError('Could not get info about cryptohome vault '
396                              'through %s. See logs for complete mount-point.'
397                              % os.path.dirname(str(mount_point)))
398    return mount_line.split()
399
400
401def __get_user_mount_info(user, allow_fail=False):
402    """Get information about the active mounts for a given user.
403
404    Returns the active mounts at the user's user and system mount points. If no
405    user is given, the active mount at the shared mount point is returned
406    (regular users have a bind-mount at this mount point for backwards
407    compatibility; the guest user has a mount at this mount point only).
408    """
409    return [__get_mount_info(mount_point=user_path(user),
410                             allow_fail=allow_fail),
411            __get_mount_info(mount_point=system_path(user),
412                             allow_fail=allow_fail)]
413
414def is_vault_mounted(user, regexes=None, allow_fail=False):
415    """Check whether a vault is mounted for the given user.
416
417    user: If no user is given, the shared mount point is checked, determining
418      whether a vault is mounted for any user.
419    regexes: dictionary of regexes to matches against the mount information.
420      The mount filesystem for the user's user and system mounts point must
421      match one of the keys.
422      The mount source point must match the selected device regex.
423
424    In addition, if mounted over ext4, we check the directory is encrypted.
425    """
426    if regexes is None:
427        regexes = {
428            constants.CRYPTOHOME_FS_REGEX_ANY :
429               constants.CRYPTOHOME_DEV_REGEX_ANY
430        }
431    user_mount_info = __get_user_mount_info(user=user, allow_fail=allow_fail)
432    for mount_info in user_mount_info:
433        # Look at each /proc/../mount lines that match mount point for a given
434        # user user/system mount (/home/user/.... /home/root/...)
435
436        # We should have at least 3 arguments (source, mount, type of mount)
437        if len(mount_info) < 3:
438            return False
439
440        device_regex = None
441        for fs_regex in regexes.keys():
442            if re.match(fs_regex, mount_info[2]):
443                device_regex = regexes[fs_regex]
444                break
445
446        if not device_regex:
447            # The thrid argument in not the expectd mount point type.
448            return False
449
450        # Check if the mount source match the device regex: it can be loose,
451        # (anything) or stricter if we expect guest filesystem.
452        if not re.match(device_regex, mount_info[0]):
453            return False
454
455        if (re.match(constants.CRYPTOHOME_FS_REGEX_EXT4, mount_info[2])
456            and not(re.match(constants.CRYPTOHOME_DEV_REGEX_LOOP_DEVICE,
457                             mount_info[0]))):
458            # Ephemeral cryptohome uses ext4 mount from a loop device,
459            # otherwise it should be ext4 crypto. Check there is an encryption
460            # key for that directory.
461            find_key_cmd_list = ['e4crypt  get_policy %s' % (mount_info[1]),
462                                 'cut -d \' \' -f 2']
463            key = __run_cmd(' | ' .join(find_key_cmd_list))
464            cmd_list = ['keyctl show @s',
465                        'grep %s' % (key),
466                        'wc -l']
467            out = __run_cmd(' | '.join(cmd_list))
468            if int(out) != 1:
469                return False
470    return True
471
472
473def is_guest_vault_mounted(allow_fail=False):
474    """Check whether a vault is mounted for the guest user.
475       It should be a mount of an ext4 partition on a loop device
476       or be backed by tmpfs.
477    """
478    return is_vault_mounted(
479        user=GUEST_USER_NAME,
480        regexes={
481            # Remove tmpfs support when it becomes unnecessary as all guest
482            # modes will use ext4 on a loop device.
483            constants.CRYPTOHOME_FS_REGEX_EXT4 :
484                constants.CRYPTOHOME_DEV_REGEX_LOOP_DEVICE,
485            constants.CRYPTOHOME_FS_REGEX_TMPFS :
486                constants.CRYPTOHOME_DEV_REGEX_GUEST,
487        },
488        allow_fail=allow_fail)
489
490def is_permanent_vault_mounted(user, allow_fail=False):
491    """Check if user is mounted over ecryptfs or ext4 crypto. """
492    return is_vault_mounted(
493        user=user,
494        regexes={
495            constants.CRYPTOHOME_FS_REGEX_ECRYPTFS :
496                constants.CRYPTOHOME_DEV_REGEX_REGULAR_USER_SHADOW,
497            constants.CRYPTOHOME_FS_REGEX_EXT4 :
498                constants.CRYPTOHOME_DEV_REGEX_REGULAR_USER_DEVICE,
499        },
500        allow_fail=allow_fail)
501
502def get_mounted_vault_path(user, allow_fail=False):
503    """Get the path where the decrypted data for the user is located."""
504    return os.path.join(constants.SHADOW_ROOT, get_user_hash(user), 'mount')
505
506
507def canonicalize(credential):
508    """Perform basic canonicalization of |email_address|.
509
510    Perform basic canonicalization of |email_address|, taking into account that
511    gmail does not consider '.' or caps inside a username to matter. It also
512    ignores everything after a '+'. For example,
513    c.masone+abc@gmail.com == cMaSone@gmail.com, per
514    http://mail.google.com/support/bin/answer.py?hl=en&ctx=mail&answer=10313
515    """
516    if not credential:
517      return None
518
519    parts = credential.split('@')
520    if len(parts) != 2:
521        raise error.TestError('Malformed email: ' + credential)
522
523    (name, domain) = parts
524    name = name.partition('+')[0]
525    if (domain == constants.SPECIAL_CASE_DOMAIN):
526        name = name.replace('.', '')
527    return '@'.join([name, domain]).lower()
528
529
530def crash_cryptohomed():
531    # Try to kill cryptohomed so we get something to work with.
532    pid = __run_cmd('pgrep cryptohomed')
533    try:
534        pid = int(pid)
535    except ValueError, e:  # empty or invalid string
536        raise error.TestError('Cryptohomed was not running')
537    utils.system('kill -ABRT %d' % pid)
538    # CONT just in case cryptohomed had a spurious STOP.
539    utils.system('kill -CONT %d' % pid)
540    utils.poll_for_condition(
541        lambda: utils.system('ps -p %d' % pid,
542                             ignore_status=True) != 0,
543            timeout=180,
544            exception=error.TestError(
545                'Timeout waiting for cryptohomed to coredump'))
546
547
548def create_ecryptfs_homedir(user, password):
549    """Creates a new home directory as ecryptfs.
550
551    If a home directory for the user exists already, it will be removed.
552    The resulting home directory will be mounted.
553
554    @param user: Username to create the home directory for.
555    @param password: Password to use when creating the home directory.
556    """
557    unmount_vault(user)
558    remove_vault(user)
559    args = [
560            CRYPTOHOME_CMD,
561            '--action=mount_ex',
562            '--user=%s' % user,
563            '--password=%s' % password,
564            '--key_label=foo',
565            '--ecryptfs',
566            '--create']
567    logging.info(__run_cmd(' '.join(args)))
568    if not is_vault_mounted(user, regexes={
569        constants.CRYPTOHOME_FS_REGEX_ECRYPTFS :
570            constants.CRYPTOHOME_DEV_REGEX_REGULAR_USER_SHADOW
571    }, allow_fail=True):
572        raise ChromiumOSError('Ecryptfs home could not be created')
573
574
575def do_dircrypto_migration(user, password, timeout=600):
576    """Start dircrypto migration for the user.
577
578    @param user: The user to migrate.
579    @param password: The password used to mount the users vault
580    @param timeout: How long in seconds to wait for the migration to finish
581    before failing.
582    """
583    unmount_vault(user)
584    args = [
585            CRYPTOHOME_CMD,
586            '--action=mount_ex',
587            '--to_migrate_from_ecryptfs',
588            '--user=%s' % user,
589            '--password=%s' % password]
590    logging.info(__run_cmd(' '.join(args)))
591    if not __get_mount_info(temporary_mount_path(user), allow_fail=True):
592        raise ChromiumOSError('Failed to mount home for migration')
593    args = [CRYPTOHOME_CMD, '--action=migrate_to_dircrypto', '--user=%s' % user]
594    logging.info(__run_cmd(' '.join(args)))
595    utils.poll_for_condition(
596        lambda: not __get_mount_info(
597                temporary_mount_path(user), allow_fail=True),
598        timeout=timeout,
599        exception=error.TestError(
600                'Timeout waiting for dircrypto migration to finish'))
601
602
603def change_password(user, password, new_password):
604    args = [
605            CRYPTOHOME_CMD,
606            '--action=migrate_key_ex',
607            '--user=%s' % user,
608            '--old_password=%s' % password,
609            '--password=%s' % new_password]
610    out = __run_cmd(' '.join(args))
611    logging.info(out)
612    if 'Key migration succeeded.' not in out:
613        raise ChromiumOSError('Key migration failed.')
614
615
616class CryptohomeProxy(DBusClient):
617    """A DBus proxy client for testing the Cryptohome DBus server.
618    """
619    CRYPTOHOME_BUS_NAME = 'org.chromium.Cryptohome'
620    CRYPTOHOME_OBJECT_PATH = '/org/chromium/Cryptohome'
621    CRYPTOHOME_INTERFACE = 'org.chromium.CryptohomeInterface'
622    ASYNC_CALL_STATUS_SIGNAL = 'AsyncCallStatus'
623    ASYNC_CALL_STATUS_SIGNAL_ARGUMENTS = (
624        'async_id', 'return_status', 'return_code'
625    )
626    DBUS_PROPERTIES_INTERFACE = 'org.freedesktop.DBus.Properties'
627
628    # Default timeout in seconds for the D-Bus connection.
629    DEFAULT_DBUS_TIMEOUT = 30
630
631    def __init__(self, bus_loop=None, autodir=None, job=None,
632                 timeout=DEFAULT_DBUS_TIMEOUT):
633        if autodir and job:
634            # Install D-Bus protos necessary for some methods.
635            dep_dir = os.path.join(autodir, 'deps', DBUS_PROTOS_DEP)
636            job.install_pkg(DBUS_PROTOS_DEP, 'dep', dep_dir)
637            sys.path.append(dep_dir)
638
639        # Set up D-Bus main loop and interface.
640        self.main_loop = gobject.MainLoop()
641        if bus_loop is None:
642            bus_loop = DBusGMainLoop(set_as_default=True)
643        self.bus = dbus.SystemBus(mainloop=bus_loop)
644        super(CryptohomeProxy, self).__init__(self.main_loop, self.bus,
645                                              self.CRYPTOHOME_BUS_NAME,
646                                              self.CRYPTOHOME_OBJECT_PATH,
647                                              timeout)
648        self.iface = dbus.Interface(self.proxy_object,
649                                    self.CRYPTOHOME_INTERFACE)
650        self.properties = dbus.Interface(self.proxy_object,
651                                         self.DBUS_PROPERTIES_INTERFACE)
652        self.handle_signal(self.CRYPTOHOME_INTERFACE,
653                           self.ASYNC_CALL_STATUS_SIGNAL,
654                           self.ASYNC_CALL_STATUS_SIGNAL_ARGUMENTS)
655
656
657    # Wrap all proxied calls to catch cryptohomed failures.
658    def __call(self, method, *args):
659        try:
660            return method(*args, timeout=180)
661        except dbus.exceptions.DBusException, e:
662            if e.get_dbus_name() == 'org.freedesktop.DBus.Error.NoReply':
663                logging.error('Cryptohome is not responding. Sending ABRT')
664                crash_cryptohomed()
665                raise ChromiumOSError('cryptohomed aborted. Check crashes!')
666            raise e
667
668
669    def __wait_for_specific_signal(self, signal, data):
670      """Wait for the |signal| with matching |data|
671         Returns the resulting dict on success or {} on error.
672      """
673      # Do not bubble up the timeout here, just return {}.
674      result = {}
675      try:
676          result = self.wait_for_signal(signal)
677      except utils.TimeoutError:
678          return {}
679      for k in data.keys():
680          if not result.has_key(k) or result[k] != data[k]:
681            return {}
682      return result
683
684
685    # Perform a data-less async call.
686    # TODO(wad) Add __async_data_call.
687    def __async_call(self, method, *args):
688        # Clear out any superfluous async call signals.
689        self.clear_signal_content(self.ASYNC_CALL_STATUS_SIGNAL)
690        out = self.__call(method, *args)
691        logging.debug('Issued call ' + str(method) +
692                      ' with async_id ' + str(out))
693        result = {}
694        try:
695            # __wait_for_specific_signal has a 10s timeout
696            result = utils.poll_for_condition(
697                lambda: self.__wait_for_specific_signal(
698                    self.ASYNC_CALL_STATUS_SIGNAL, {'async_id' : out}),
699                timeout=180,
700                desc='matching %s signal' % self.ASYNC_CALL_STATUS_SIGNAL)
701        except utils.TimeoutError, e:
702            logging.error('Cryptohome timed out. Sending ABRT.')
703            crash_cryptohomed()
704            raise ChromiumOSError('cryptohomed aborted. Check crashes!')
705        return result
706
707
708    def mount(self, user, password, create=False, async=True, key_label='bar'):
709        """Mounts a cryptohome.
710
711        Returns True if the mount succeeds or False otherwise.
712        """
713        import rpc_pb2
714
715        acc = rpc_pb2.AccountIdentifier()
716        acc.account_id = user
717
718        auth = rpc_pb2.AuthorizationRequest()
719        auth.key.secret = password
720        auth.key.data.label = key_label
721
722        mount_req = rpc_pb2.MountRequest()
723        if create:
724            mount_req.create.copy_authorization_key = True
725
726        out = self.__call(self.iface.MountEx, acc.SerializeToString(),
727            auth.SerializeToString(), mount_req.SerializeToString())
728        parsed_out = rpc_pb2.BaseReply()
729        parsed_out.ParseFromString(''.join(map(chr, out)))
730        return parsed_out.error == rpc_pb2.CRYPTOHOME_ERROR_NOT_SET
731
732
733    def unmount(self, user):
734        """Unmounts a cryptohome.
735
736        Returns True if the unmount suceeds or false otherwise.
737        """
738        import rpc_pb2
739
740        req = rpc_pb2.UnmountRequest()
741
742        out = self.__call(self.iface.UnmountEx, req.SerializeToString())
743        parsed_out = rpc_pb2.BaseReply()
744        parsed_out.ParseFromString(''.join(map(chr, out)))
745        return parsed_out.error == rpc_pb2.CRYPTOHOME_ERROR_NOT_SET
746
747
748    def is_mounted(self, user):
749        """Tests whether a user's cryptohome is mounted."""
750        return (utils.is_mountpoint(user_path(user))
751                and utils.is_mountpoint(system_path(user)))
752
753
754    def require_mounted(self, user):
755        """Raises a test failure if a user's cryptohome is not mounted."""
756        utils.require_mountpoint(user_path(user))
757        utils.require_mountpoint(system_path(user))
758
759
760    def remove(self, user, async=True):
761        """Removes a users cryptohome.
762
763        Returns True if the operation succeeds or False otherwise.
764        """
765        import rpc_pb2
766
767        acc = rpc_pb2.AccountIdentifier()
768        acc.account_id = user
769
770        out = self.__call(self.iface.RemoveEx, acc.SerializeToString())
771        parsed_out = rpc_pb2.BaseReply()
772        parsed_out.ParseFromString(''.join(map(chr, out)))
773        return parsed_out.error == rpc_pb2.CRYPTOHOME_ERROR_NOT_SET
774
775
776    def ensure_clean_cryptohome_for(self, user, password=None):
777        """Ensure a fresh cryptohome exists for user.
778
779        @param user: user who needs a shiny new cryptohome.
780        @param password: if unset, a random password will be used.
781        """
782        if not password:
783            password = ''.join(random.sample(string.ascii_lowercase, 6))
784        self.remove(user)
785        self.mount(user, password, create=True)
786
787    def lock_install_attributes(self, attrs):
788        """Set and lock install attributes for the device.
789
790        @param attrs: dict of install attributes.
791        """
792        take_tpm_ownership()
793        self.wait_for_install_attributes_ready()
794        for key, value in attrs.items():
795            if not self.__call(self.iface.InstallAttributesSet, key,
796                               dbus.ByteArray(value + '\0')):
797                return False
798        return self.__call(self.iface.InstallAttributesFinalize)
799
800    def wait_for_install_attributes_ready(self):
801        """Wait until install attributes are ready.
802        """
803        utils.poll_for_condition(
804            lambda: self.__call(self.iface.InstallAttributesIsReady),
805            timeout=300,
806            exception=error.TestError(
807                    'Timeout waiting for install attributes are ready'))
808