blob: e990de0ac80d5eb6a4bc166fea1bc655f11de25b [file] [log] [blame]
#!/bin/bash
# Copyright (c) 2009 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.
#
# A collection of functions which implement the bare
# minimum of functionality for creating, mounting, and
# unmounting sparse image files with dmsetup.
# Declare the executable dependencies for this code.
# This includes bash builtins so we can stub them.
utils::declare_commands read echo mkdir test mount grep blockdev
utils::declare_commands pkill chown df tr cut dd cat openssl ls
utils::declare_commands losetup dmsetup tune2fs chmod xxd exec
utils::declare_commands head tail cp umount touch rm date true sleep
# Be sure to use our e4fsprogs instead of the stable version.
export PATH=/usr/lib/e4fsprogs-git/bin:${PATH}
utils::declare_commands resize2fs e4defrag e2fsck mkfs.ext4 &> /dev/null
# TODO: deal with missing commands..
function cryptohome::log() {
$echo "$($date +%s)[$$]: $@" >> $LOG_FILE
}
function cryptohome::is_mounted() {
local mountpt="${1:-$DEFAULT_MOUNT_POINT}"
# TODO: we should make sure there is no trailing slash
# We dont care about mounting over tmpfs if we have to.
if $mount | $grep "$mountpt" | $grep -qv tmpfs; then
return 0
else
return 1
fi
}
# unmount [mountpt] [username]
function cryptohome::unmount() {
local mountpt="${1:-$DEFAULT_MOUNT_POINT}"
local user="${2:-$DEFAULT_USER}"
cryptohome::log "unmount start"
$pkill -9 -u $user && $true &> /dev/null
$umount "$mountpt"
cryptohome::close
cryptohome::detach
# Make sure the mountpoint can't be used on accident by a faulty log in.
# TODO: enable this when the default login goes away.
#$chown root:root "$mountpt"
cryptohome::log "unmount finished"
}
# total_blocks [some_path]
function cryptohome::total_blocks() {
local target="${1:-/home}"
local disk_size="$($df -P -B $BLOCK_SIZE "$target" |
$tr -s ' ' |
$grep -v Filesystem |
$cut -f2 -d' ')"
if [[ -z "$disk_size" ]]; then
echo 0
echo "Disk appears to be less than 1G or df output is unparseable!" 1>&2
return 1
fi
echo $disk_size
return 0
}
# make_table masterkey [loopdev]
function cryptohome::make_table() {
local masterkey="$1"
local loopdev="${2:-$DEFAULT_LOOP_DEVICE}"
[[ $# -lt 1 ]] && return 1 # argument sanity check
echo "0 $($blockdev --getsize $loopdev) crypt aes-cbc-essiv:sha256 "$masterkey" 0 $loopdev 0"
}
# maximize_image /path/to/image.img
function cryptohome::maximize_image() {
local image="$1"
local blocks="$(cryptohome::total_blocks)" # looks under home mount
local current="$($ls --block-size=$BLOCK_SIZE -s "$image" | $cut -f1 -d' ')"
# Round down by one bg to avoid trying to resize past the end of the fs
blocks=$((blocks - BLOCKS_IN_A_GROUP))
if [[ "$blocks" -le "$current" ]]; then
return 0
fi
# This will create a file if it doesn't exist or expand it to full size.
$dd if=/dev/zero of="$image" count=0 bs=$BLOCK_SIZE seek=$blocks
}
# maximize_fs /path/to/image.img masterkey [online] [loopdev] [mapperdev]
# performs an online resize of the filesystem. Does not update the image.
function cryptohome::maximize_fs() {
local image="$1"
local masterkey="$2"
local mountpt="$3"
local loopdev="${4:-$DEFAULT_LOOP_DEVICE}"
local mapperdev="${5:-$DEFAULT_MAPPER_DEVICE}"
[[ $# -lt 2 ]] && return 1 # argument sanity check
cryptohome::log "maximize_fs start"
if [[ -z "$mountpt" ]]; then
cryptohome::check -f
# Do it as quickly as possible if we are offline.
$resize2fs $mapperdev
else
local blocks="$(cryptohome::total_blocks $image)"
local current="$(cryptohome::total_blocks $mountpt)"
blocks=$((blocks - BLOCKS_IN_A_GROUP))
if [[ "$blocks" -le "$current" ]]; then
cryptohome::log "no work to do: $blocks <= $current"
cryptohome::log "maximize_fs end"
return 0
fi
# If we're online, we don't want to saturate the I/O and it's
# okay if it doesn't complete. So we add a block group at a time.
# With lazy inode tables, this isn't adding much data to disk, but
# it will blast several megabytes directly to disk if the image is
# quite large. For small images, the metadata needed is very little,
# but this rate limited resize won't take long either.
local next_blocks=$((current + BLOCKS_IN_A_GROUP))
$sleep 3 # TODO(wad) make configurable
while [[ "$next_blocks" -lt "$blocks" ]]; do
$sleep 0.3 # TODO(wad) make configurable
$resize2fs -f $mapperdev ${next_blocks} || true
next_blocks=$((next_blocks + BLOCKS_IN_A_GROUP))
done
fi
cryptohome::log "maximize_fs end"
}
# attach /path/to/image [loop device]
function cryptohome::attach() {
local image="$1"
local loopdev="${2:-$DEFAULT_LOOP_DEVICE}"
[[ $# -lt 1 ]] && return 1 # argument sanity check
$losetup "$loopdev" "$image"
}
# detach [loop device]
function cryptohome::detach() {
local loopdev="${1:-$DEFAULT_LOOP_DEVICE}"
$losetup -d "$loopdev"
}
# format num_blocks max_resize_blocks [target mapper device] [loop device]
function cryptohome::format() {
local blocks="$1"
local resize_blocks="$2"
local mapperdev="${3:-$DEFAULT_MAPPER_DEVICE}"
local loopdev="${4:-$DEFAULT_LOOP_DEVICE}"
[[ $# -lt 1 ]] && return 1 # argument sanity check
$mkfs__ext4 -b $BLOCK_SIZE \
-O ^huge_file \
-E lazy_itable_init=1,resize=$resize_blocks \
"$mapperdev" "$blocks"
$tune2fs -c -1 -i 0 "$mapperdev" # we'll be checking later.
}
# password_to_wrapper password salt_file [iteration_count]
# Create key from the passphrase using a per-user salt and
# an arbitrary iteration count for optional key strengthening.
function cryptohome::password_to_wrapper() {
local password="$1"
local salt_file="$2"
local itercount="${3:-1}"
local wrapped="$password"
local count=0
[[ $# -lt 2 ]] && return 1 # argument sanity check
if [[ ! -f "$salt_file" ]]; then
$head -c 16 /dev/urandom > $salt_file
fi
while [[ $count -lt "$itercount" ]]; do
wrapped="$($cat "$salt_file" <($echo -n "$wrapped") |
$openssl sha1)"
count=$((count+1))
done
$echo "$wrapped"
}
# master_key user_password userid [wrapped_keyfile]
function cryptohome::unwrap_master_key() {
local password="$1"
local userid="$2"
[[ $# -lt 2 ]] && return 1 # argument sanity check
local keyfile="${3:-$IMAGE_DIR/$userid/$KEY_FILE_USER_ZERO}"
local wrapper="$(cryptohome::password_to_wrapper \
"$password" "${keyfile}.salt")"
$openssl aes-256-ecb \
-in "$keyfile" -kfile <($echo -n "$wrapper") -md sha1 -d
}
# create_master_key user_password userid [wrapped_keyfile] [iters]
function cryptohome::create_master_key() {
local password="$1"
local userid="$2"
[[ $# -lt 2 ]] && return 1 # argument sanity check
local keyfile="${3:-$IMAGE_DIR/$userid/$KEY_FILE_USER_ZERO}"
local iters="${4:-1}"
local wrapper="$(cryptohome::password_to_wrapper \
"$password" "${keyfile}.salt" "$iters")"
local master_key="$($xxd -ps -l $KEY_SIZE -c $KEY_SIZE /dev/urandom)"
# openssl salts itself too, but this lets us do repeated iterations.
$openssl aes-256-ecb -out "$keyfile" -kfile <($echo -n "$wrapper") -md sha1 -e < <(echo -n $master_key)
$echo -n "$master_key"
}
# open masterkey [mapper dev] [loop dev]
function cryptohome::open() {
local masterkey="$1"
local mapperdev="${2:-$DEFAULT_MAPPER_DEVICE}"
local loopdev="${3:-$DEFAULT_LOOP_DEVICE}"
$dmsetup create "${mapperdev//*\/}" <(cryptohome::make_table "$masterkey")
}
# close [mapper dev]
function cryptohome::close() {
local mapperdev="${1:-$DEFAULT_MAPPER_DEVICE}"
$dmsetup remove -f "${mapperdev//*\/}"
}
# is_opened [mapper dev]
function cryptohome::is_opened() {
local mapperdev="${1:-$DEFAULT_MAPPER_DEVICE}"
if $test -b "$mapperdev"; then
return 0
else
return 1
fi
}
# is_attached [loop dev]
function cryptohome::is_attached() {
local loopdev="${1:-$DEFAULT_LOOP_DEVICE}"
if $test -b "$loopdev"; then
return 0
else
return 1
fi
}
# mount [mapper device] [mount point]
function cryptohome::mount() {
local mapperdev="${1:-$DEFAULT_MAPPER_DEVICE}"
local mountpt="${2:-$DEFAULT_MOUNT_POINT}"
$mount -o "$MOUNT_OPTIONS" "$mapperdev" "$mountpt"
}
# check [check argument] [mapper device]
function cryptohome::check() {
local arg="${1:-}"
local mapperdev="${2:-$DEFAULT_MAPPER_DEVICE}"
$e2fsck $arg -p "$mapperdev"
}
# update_skel [mount point]
function cryptohome::update_skel() {
local mountpt="${1:-$DEFAULT_MOUNT_POINT}"
$mkdir -p $IMAGE_DIR/skel
$mkdir -p $IMAGE_DIR/skel/logs
$cp -ru /etc/skel/. $IMAGE_DIR/skel/
$chown -R $DEFAULT_USER:$DEFAULT_USER $IMAGE_DIR/skel
$cp -ru $IMAGE_DIR/skel/. "$mountpt"
# This is temporary until we replace the script. Otherwise
# users can cross-contaminate each other's encrypted stores.
return 0
}
function cryptohome::check_and_clear_loop() {
# TODO: use losetup -f explicitly and clean up on failure
if [[ "$($losetup -f)" != "$DEFAULT_LOOP_DEVICE" ]]; then
cryptohome::log "$DEFAULT_LOOP_DEVICE is unavailable!"
if cryptohome::is_mounted; then
cryptohome::log "attempting to unmount lingering mount"
cryptohome::unmount
fi
if cryptohome::is_opened; then
cryptohome::log "attempting to close a lingering dm device"
cryptohome::close
fi
if cryptohome::is_attached; then
cryptohome::log "attempting to detach the loop device"
cryptohome::detach
fi
if [[ "$($losetup -f)" != "$DEFAULT_LOOP_DEVICE" ]]; then
cryptohome::log "$DEFAULT_LOOP_DEVICE could not be freed."
return 1
fi
cryptohome::log "default loop device freed for use"
fi
return 0
}
# mount_or_create userid password
function cryptohome::mount_or_create() {
local userid="$1"
local password="$2"
[[ $# -lt 2 ]] && return 1 # argument sanity check
local image="$IMAGE_DIR/${userid}/image"
IMAGE="$image" # exported for use by cleanup handlers/logging
# Ensure a sane environment.
if [[ ! -d "$IMAGE_DIR/$userid" ]]; then
$mkdir -p "$IMAGE_DIR/$userid"
fi
# We need a master key file and an image
if [[ -f "$image" && -f "$IMAGE_DIR/$userid/$KEY_FILE_USER_ZERO" ]]; then
cryptohome::log "mount start"
if ! cryptohome::check_and_clear_loop; then
cryptohome::log "mount_or_create bailing"
return 1
fi
cryptohome::maximize_image "$image"
cryptohome::attach "$image"
local masterkey="$(cryptohome::unwrap_master_key "$password" "$userid")"
# TODO: we should track mount attempts so we can delete a broken mount.
# right now, we will just fail forever.
# So if a user image gets in a wedged state they get stuck in tmpfs
# land.
cryptohome::open "$masterkey"
# checking is not forced and will only impact login time
# if there is a filesystem error. However, we don't have
# a way to give a user feedback.
# TODO: add UI or determine if we should just re-image.
# Filesystem checking is disabled for Indy to further minimize initial
# login impact. However, we need to determine our priorities in this
# area.
# cryptohome::check
cryptohome::mount
cryptohome::update_skel
$chown $DEFAULT_USER $DEFAULT_MOUNT_POINT
$chmod 750 $DEFAULT_MOUNT_POINT
# Perform an online resize behind the scenes just in case it
# wasn't completed before.
trap - ERR # disable any potential err handlers
cryptohome::maximize_fs "$image" "$masterkey" "$DEFAULT_MOUNT_POINT" &
disown -a
cryptohome::log "mount end"
else
cryptohome::log "create_and_mount start"
# Creates a sparse file of the maximum ever possible on the given partition
cryptohome::maximize_image "$image"
if ! cryptohome::check_and_clear_loop; then
cryptohome::log "mount_or_create bailing"
return 1
fi
cryptohome::attach "$image"
local masterkey="$(cryptohome::create_master_key "$password" "$userid")"
cryptohome::open "$masterkey"
# Initially, just format to around 131m then resize online to bring the
# filesystem up as soon as possible.
cryptohome::format "$BLOCKS_IN_A_GROUP" "$(cryptohome::total_blocks $image)"
cryptohome::mount
cryptohome::update_skel
$chown -R $DEFAULT_USER $DEFAULT_MOUNT_POINT
$chmod 750 $DEFAULT_MOUNT_POINT
# Perform an online resize behind the scenes
# and remove the retry trap.
trap - ERR # disable any potential err handlers
cryptohome::maximize_fs "$image" "$masterkey" "$DEFAULT_MOUNT_POINT" &
disown -a
cryptohome::log "create_and_mount end"
fi
return 0
}