blob: 2ec3bc43327aa964ea937be67c750ca1ec5de029 [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"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/shirou/gopsutil/disk"
"chromiumos/tast/errors"
"chromiumos/tast/local/testexec"
"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"
)
// 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(user string) (string, error) {
p, err := UserPath(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(user string) (string, error) {
b, err := exec.Command("cryptohome-path", "user", user).Output()
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.
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
}
// logStatus logs information about cryptohome's status.
// TODO(derat): Delete this after https://crbug.com/864282 is resolved.
func logStatus(ctx context.Context) {
cmd := testexec.CommandContext(ctx, "cryptohome", "--action=status")
if b, err := cmd.Output(); err != nil {
testing.ContextLog(ctx, "Failed to get cryptohome status")
cmd.DumpLog(ctx)
} else {
testing.ContextLog(ctx, "cryptohome status:\n", strings.TrimSpace(string(b)))
}
for _, p := range []string{"/sys/class/tpm/tpm0/device/owned", "/sys/class/misc/tpm0/device/owned"} {
if _, err := os.Stat(p); err == nil {
if b, err := ioutil.ReadFile(p); err == nil {
testing.ContextLogf(ctx, "%v contains %q", p, strings.TrimSpace(string(b)))
} else {
testing.ContextLogf(ctx, "Failed to read %v: %v", p, err)
}
}
}
}
// 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 {
validatePartition := validatePermanentPartition
if user == GuestUser {
validatePartition = validateGuestPartition
}
userpath, err := UserPath(user)
if err != nil {
return err
}
systempath, err := SystemPath(user)
if err != nil {
return err
}
// Reserve a bit of time to log the status before ctx's deadline.
// TODO(derat): Delete this after https://crbug.com/864282 is resolved.
var timeout time.Duration
if dl, ok := ctx.Deadline(); ok {
timeout = dl.Sub(time.Now()) - (3 * time.Second) // testing.Poll ignores negative timeouts
}
testing.ContextLogf(ctx, "Waiting for cryptohome for user %q", user)
err = testing.Poll(ctx, func(ctx context.Context) error {
partitions, err := disk.Partitions(true /* all */)
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: timeout, Interval: mountPollInterval})
if err != nil {
logStatus(ctx)
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(user)
if err != nil {
return err
}
if _, err := os.Stat(filepath.Join(shadowRoot, hash)); err != nil {
return err
}
return nil
}, &testing.PollOptions{Timeout: 30 * 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(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) {
validatePartition := validatePermanentPartition
if user == GuestUser {
validatePartition = validateGuestPartition
}
userpath, err := UserPath(user)
if err != nil {
return false, err
}
systempath, err := SystemPath(user)
if err != nil {
return false, err
}
partitions, err := disk.Partitions(true /* all */)
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
}