blob: a404cd34aad451b00edf5291279e47e4ffad0c06 [file] [log] [blame]
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package cryptohome operates on encrypted home directories.
package cryptohome
import (
"context"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/shirou/gopsutil/disk"
"github.com/shirou/gopsutil/process"
"chromiumos/tast/errors"
"chromiumos/tast/local/dbusutil"
"chromiumos/tast/local/testexec"
"chromiumos/tast/local/upstart"
"chromiumos/tast/testing"
)
const (
// mountPollInterval contains the delay between WaitForUserMount's parses of mtab.
mountPollInterval = 10 * time.Millisecond
// GuestUser is the name representing a guest user account.
// Defined in libbrillo/brillo/cryptohome.cc.
GuestUser = "$guest"
// mounterExe is the full executable for the out-of-process cryptohome
// mounter.
// Defined in cryptohome/BUILD.gn.
mounterExe = "/usr/sbin/cryptohome-namespace-mounter"
// cryptohomedExe is the full path of the daemon cryptohomed.
//
// TODO(crbug.com/1074735): Remove this and use mounterExe above for normal user mounts as well
// once the out-of-process cryptohome mounter is ready for that.
cryptohomedExe = "/usr/sbin/cryptohomed"
)
// hashRegexp extracts the hash from a cryptohome dir's path.
var hashRegexp *regexp.Regexp
var shadowRegexp *regexp.Regexp // matches a path to vault under /home/shadow.
var devRegexp *regexp.Regexp // matches a path to /dev/*.
var devLoopRegexp *regexp.Regexp // matches a path to /dev/loop\d+.
const shadowRoot = "/home/.shadow" // is a root directory of vault.
func init() {
hashRegexp = regexp.MustCompile("^/home/user/([[:xdigit:]]+)$")
shadowRegexp = regexp.MustCompile(`^/home/\.shadow/[^/]*/vault$`)
devRegexp = regexp.MustCompile(`^/dev/[^/]*$`)
devLoopRegexp = regexp.MustCompile(`^/dev/loop[0-9]+$`)
}
// UserHash returns user's cryptohome hash.
func UserHash(ctx context.Context, user string) (string, error) {
p, err := UserPath(ctx, user)
if err != nil {
return "", err
}
m := hashRegexp.FindStringSubmatch(p)
if m == nil {
return "", errors.Errorf("didn't find hash in path %q", p)
}
return m[1], nil
}
// UserPath returns the path to user's encrypted home directory.
func UserPath(ctx context.Context, user string) (string, error) {
b, err := testexec.CommandContext(ctx, "cryptohome-path", "user", user).Output(testexec.DumpLogOnError)
if err != nil {
return "", err
}
return strings.TrimSpace(string(b)), nil
}
// SystemPath returns the path to user's encrypted system directory.
func SystemPath(user string) (string, error) {
b, err := exec.Command("cryptohome-path", "system", user).Output()
if err != nil {
return "", err
}
return strings.TrimSpace(string(b)), nil
}
// RemoveUserDir removes a user's encrypted home directory.
// Success is reported if the user directory doesn't exist,
// but an error will be returned if the user is currently logged in.
func RemoveUserDir(ctx context.Context, user string) error {
testing.ContextLog(ctx, "Removing cryptohome for ", user)
cmd := testexec.CommandContext(ctx, "cryptohome", "--action=remove", "--force", "--user="+user)
if err := cmd.Run(); err != nil {
cmd.DumpLog(ctx)
return errors.Wrap(err, "failed to remove cryptohome")
}
return nil
}
// findMounterPID finds the pid of the given mounter process.
func findMounterPID(mounter string) (int32, error) {
procs, err := process.Processes()
if err != nil {
return -1, errors.Wrap(err, "could not list running processes")
}
for _, proc := range procs {
if exe, err := proc.Exe(); err == nil && exe == mounter {
return proc.Pid, nil
}
}
// If the mounter process is not found, don't return an error.
return -1, nil
}
// findMountsForPID returns the list of mounts in pid's mount namespace.
func findMountsForPID(pid int32) ([]disk.PartitionStat, error) {
path := fmt.Sprintf("/proc/%d/mounts", pid)
bs, err := ioutil.ReadFile(path)
if err != nil {
return nil, errors.Wrapf(err, "failed to get list of mounts for pid %d", pid)
}
output := strings.Trim(string(bs), "\n")
mounts := strings.Split(output, "\n")
res := make([]disk.PartitionStat, 0, len(mounts))
for _, mount := range mounts {
var d disk.PartitionStat
fields := strings.Fields(mount)
d = disk.PartitionStat{
Device: fields[0],
Mountpoint: fields[1],
Fstype: fields[2],
Opts: fields[3],
}
res = append(res, d)
}
return res, nil
}
// findMounts returns the list of mounts in the given mounter's mount namespace.
func findMounts(mounter string) ([]disk.PartitionStat, error) {
mounterPID, err := findMounterPID(mounter)
if err != nil {
return nil, errors.Wrap(err, "could not list running processes")
} else if mounterPID == -1 {
// If the mounter process is not running, the list of mounts is
// empty.
return nil, nil
}
return findMountsForPID(mounterPID)
}
// findPartition returns a pointer to the entry in ps corresponding to path,
// or nil if no matching entry is present.
func findPartition(ps []disk.PartitionStat, path string) *disk.PartitionStat {
for i := range ps {
if ps[i].Mountpoint == path {
return &ps[i]
}
}
return nil
}
// validatePermanentPartition checks if the given partition is valid for a
// (non-guest) user mount. Returns nil on success, or reports an error.
func validatePermanentPartition(p *disk.PartitionStat) error {
switch p.Fstype {
case "ext4":
if !devRegexp.MatchString(p.Device) || devLoopRegexp.MatchString(p.Device) {
return errors.Errorf("ext4 device %q should match %q excluding %q", p.Device, devRegexp, devLoopRegexp)
}
case "ecryptfs":
if !shadowRegexp.MatchString(p.Device) {
return errors.Errorf("ecryptfs device %q should match %q", p.Device, shadowRegexp)
}
default:
return errors.Errorf("unexpected file system: %q", p.Fstype)
}
return nil
}
// validateGuestPartition checks if the given partition is valid for a guest
// user mount. Returns nil on success, or reports an error.
func validateGuestPartition(p *disk.PartitionStat) error {
switch p.Fstype {
case "ext4":
if !devLoopRegexp.MatchString(p.Device) {
return errors.Errorf("ext4 device %q should match %q", p.Device, devLoopRegexp)
}
case "tmpfs":
if p.Device != "guestfs" {
return errors.Errorf("tmpfs device %q should be guestfs", p.Device)
}
default:
return errors.Errorf("unexpected file system: %q", p.Fstype)
}
return nil
}
// WaitForUserMount waits for user's encrypted home directory to be mounted.
func WaitForUserMount(ctx context.Context, user string) error {
mounter := cryptohomedExe
validatePartition := validatePermanentPartition
if user == GuestUser {
mounter = mounterExe
validatePartition = validateGuestPartition
}
userpath, err := UserPath(ctx, user)
if err != nil {
return err
}
systempath, err := SystemPath(user)
if err != nil {
return err
}
const waitTimeout = 30 * time.Second
testing.ContextLogf(ctx, "Waiting for cryptohome for user %q with timeout %v", user, waitTimeout)
err = testing.Poll(ctx, func(ctx context.Context) error {
partitions, err := findMounts(mounter)
if err != nil {
return err
}
up := findPartition(partitions, userpath)
if up == nil {
return errors.Errorf("%v not found", userpath)
}
if err = validatePartition(up); err != nil {
return err
}
sp := findPartition(partitions, systempath)
if sp == nil {
return errors.Errorf("%v not found", systempath)
}
if err = validatePartition(sp); err != nil {
return err
}
return nil
}, &testing.PollOptions{Timeout: waitTimeout, Interval: mountPollInterval})
if err != nil {
return errors.Wrapf(err, "not mounted for %s", user)
}
return nil
}
// CreateVault creates the vault for the user with given password.
func CreateVault(ctx context.Context, user, password string) error {
testing.ContextLogf(ctx, "Creating vault mount for user %q", user)
err := testing.Poll(ctx, func(ctx context.Context) error {
cmd := testexec.CommandContext(
ctx, "cryptohome", "--action=mount_ex",
"--user="+user, "--password="+password,
"--async", "--create", "--key_label=bar")
if err := cmd.Run(); err != nil {
cmd.DumpLog(ctx)
return err
}
// TODO(crbug.com/690994): Remove this additional call to
// UserHash when crbug.com/690994 is fixed.
hash, err := UserHash(ctx, user)
if err != nil {
return err
}
if _, err := os.Stat(filepath.Join(shadowRoot, hash)); err != nil {
return err
}
return nil
}, &testing.PollOptions{Timeout: 60 * time.Second, Interval: 1 * time.Second})
if err != nil {
return errors.Wrapf(err, "failed to create vault for %s", user)
}
return nil
}
// RemoveVault removes the vault for the user.
func RemoveVault(ctx context.Context, user string) error {
hash, err := UserHash(ctx, user)
if err != nil {
return err
}
testing.ContextLogf(ctx, "Removing vault for user %q", user)
cmd := testexec.CommandContext(
ctx, "cryptohome", "--action=remove", "--force", "--user="+user)
if err := cmd.Run(); err != nil {
return errors.Wrapf(err, "failed to remove vault for %q", user)
}
// Ensure that the vault does not exist.
if _, err := os.Stat(filepath.Join(shadowRoot, hash)); !os.IsNotExist(err) {
return errors.Wrapf(err, "cryptohome could not remove vault for user %q", user)
}
return nil
}
// UnmountVault unmounts the vault for the user.
func UnmountVault(ctx context.Context, user string) error {
testing.ContextLogf(ctx, "Unmounting vault for user %q", user)
cmd := testexec.CommandContext(ctx, "cryptohome", "--action=unmount")
if err := cmd.Run(); err != nil {
return errors.Wrapf(err, "failed to unmount vault for user %q", user)
}
if mounted, err := IsMounted(ctx, user); err == nil && mounted {
return errors.Errorf("cryptohome did not unmount user %q", user)
}
return nil
}
// IsMounted checks if the vault for the user is mounted.
func IsMounted(ctx context.Context, user string) (bool, error) {
mounter := cryptohomedExe
validatePartition := validatePermanentPartition
if user == GuestUser {
mounter = mounterExe
validatePartition = validateGuestPartition
}
userpath, err := UserPath(ctx, user)
if err != nil {
return false, err
}
systempath, err := SystemPath(user)
if err != nil {
return false, err
}
partitions, err := findMounts(mounter)
if err != nil {
return false, err
}
up := findPartition(partitions, userpath)
if up == nil {
return false, nil
}
if err = validatePartition(up); err != nil {
return false, nil
}
sp := findPartition(partitions, systempath)
if sp == nil {
return false, nil
}
if err = validatePartition(sp); err != nil {
return false, nil
}
return true, nil
}
// MountGuest sends a request to cryptohome to create a mount point for a
// guest user.
func MountGuest(ctx context.Context) error {
testing.ContextLog(ctx, "Mounting guest cryptohome")
cmd := testexec.CommandContext(ctx, "cryptohome", "--action=mount_guest_ex")
if err := cmd.Run(); err != nil {
cmd.DumpLog(ctx)
return errors.Wrap(err, "failed to request mounting guest vault")
}
if err := WaitForUserMount(ctx, GuestUser); err != nil {
return errors.Wrap(err, "failed to mount guest vault")
}
return nil
}
// CheckService performs high-level verification of cryptohomed.
// If an error is returned, CheckDeps can be called to return additional
// information pointing to the cause of the problem.
func CheckService(ctx context.Context) error {
if err := upstart.CheckJob(ctx, "cryptohomed"); err != nil {
return err
}
bus, err := dbusutil.SystemBus()
if err != nil {
return errors.Wrap(err, "failed to connect to D-Bus system bus")
}
const (
svcName = "org.chromium.Cryptohome"
// TODO(crbug.com/879797): decrease the timeout value once cryptohome D-Bus service
// startup latency is reduced.
svcTimeout = 25 * time.Second
)
svcCtx, svcCancel := context.WithTimeout(ctx, svcTimeout)
defer svcCancel()
if err := dbusutil.WaitForService(svcCtx, bus, svcName); err != nil {
return errors.Wrapf(err, "%v D-Bus service unavailable", svcName)
}
return nil
}
// CheckDeps checks services that cryptohomed depends on and returns a list of potential problems.
// It can be used to collect more detail after CheckService reports an error.
func CheckDeps(ctx context.Context) (errs []error) {
if out, err := testexec.CommandContext(ctx, "tpmc", "tpmver").Output(); err != nil {
errs = append(errs, errors.Wrap(err, "unknown TPM version"))
} else {
version := strings.TrimSpace(string(out))
switch version {
case "1.2":
// TPM 1.2 systems use the trousers library rather than a daemon.
case "2.0":
for _, job := range []string{"attestationd", "tpm_managerd", "trunksd"} {
if err := upstart.CheckJob(ctx, job); err != nil {
errs = append(errs, err)
}
}
}
}
// chapsd should be running unconditionally.
if err := upstart.CheckJob(ctx, "chapsd"); err != nil {
errs = append(errs, err)
}
return errs
}