You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
382 lines
15 KiB
382 lines
15 KiB
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# luks-img.sh — manage LUKS-encrypted .img files (loop, open, mount)
|
|
# Now with:
|
|
# launch-vbox — attach unlocked mapper to a VirtualBox VM (raw VMDK wrapper)
|
|
# detach-vbox — detach & remove the wrapper
|
|
# attach-virt — attach unlocked mapper to a libvirt/QEMU VM
|
|
# detach-virt — detach it again
|
|
#
|
|
# Requires: cryptsetup, losetup, mount, umount, findmnt, blkid
|
|
# Optional: VBoxManage (VirtualBox), virsh (libvirt)
|
|
|
|
die() { echo "ERROR: $*" >&2; exit 1; }
|
|
need() { command -v "$1" >/dev/null 2>&1 || die "Missing dependency: $1"; }
|
|
banner() { echo -e "\n== $* ==\n"; }
|
|
|
|
SUDO=${SUDO:-}
|
|
as_root() {
|
|
if [[ $EUID -ne 0 ]]; then
|
|
if [[ -z "${SUDO}" ]]; then
|
|
exec sudo SUDO=1 -- "$0" "$@"
|
|
else
|
|
die "Escalation loop detected; aborting."
|
|
fi
|
|
fi
|
|
}
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
luks-img.sh — Create/open/mount/close LUKS-encrypted disk images (.img)
|
|
|
|
USAGE:
|
|
luks-img.sh create --file FILE.img --size 4G [--fs ext4] [--name NAME] [--mount /mnt/foo]
|
|
[--cipher aes-xts-plain64 --key-size 512 --hash sha512 --pbkdf argon2id]
|
|
[--keyfile /path/key] [--sparse] [--no-mkfs]
|
|
luks-img.sh open --file FILE.img [--name NAME] [--mount /mnt/foo] [--keyfile /path/key]
|
|
luks-img.sh close --file FILE.img [--name NAME]
|
|
luks-img.sh status --name NAME
|
|
luks-img.sh dump --file FILE.img
|
|
luks-img.sh add-key --file FILE.img --keyfile /path/new_key [--existing-keyfile /path/key]
|
|
luks-img.sh header-backup --file FILE.img --out FILE.header
|
|
luks-img.sh header-restore --file FILE.img --in FILE.header
|
|
|
|
# NEW: VirtualBox & virt-manager helpers
|
|
luks-img.sh launch-vbox --file FILE.img --vm "VM Name" [--name NAME] [--keyfile /path/key] \
|
|
[--port 0] [--controller "SATA Controller"] [--start]
|
|
luks-img.sh detach-vbox --vm "VM Name" [--port 0] [--controller "SATA Controller"] [--wrapper PATH.vmdk]
|
|
|
|
luks-img.sh attach-virt --file FILE.img --vm myvm [--name NAME] [--keyfile /path/key] \
|
|
[--target vdb] [--bus virtio] [--persistent]
|
|
luks-img.sh detach-virt --vm myvm [--target vdb] [--persistent]
|
|
|
|
NOTES:
|
|
- NAME defaults to basename(FILE) without extension (secure.img -> secure).
|
|
- 'launch-vbox' creates a raw VMDK wrapper pointing at /dev/mapper/NAME, attaches it, and can --start the VM.
|
|
- 'detach-vbox' removes that attachment and deletes the wrapper file (path is shown by launch-vbox).
|
|
- 'attach-virt' uses virsh attach-disk to add /dev/mapper/NAME to a libvirt VM (virt-manager will show it).
|
|
- 'detach-virt' removes that attachment.
|
|
|
|
EXAMPLES:
|
|
sudo ./luks-img.sh create --file secure.img --size 8G --mount /mnt/secure
|
|
sudo ./luks-img.sh open --file secure.img --mount /mnt/secure
|
|
sudo ./luks-img.sh launch-vbox --file secure.img --vm "Ubuntu VM" --start
|
|
sudo ./luks-img.sh attach-virt --file secure.img --vm myvm --target vdb --persistent
|
|
sudo ./luks-img.sh detach-vbox --vm "Ubuntu VM"
|
|
sudo ./luks-img.sh detach-virt --vm myvm --target vdb --persistent
|
|
EOF
|
|
exit 1
|
|
}
|
|
|
|
# ---------- tiny arg helpers ----------
|
|
expand_kv() { [[ "$1" == --*=* ]] && { k="${1%%=*}"; v="${1#*=}"; set -- "$k" "$v" "${@:2}"; echo "$@"; return 0; }; echo "$@"; }
|
|
|
|
# ---------- core helpers ----------
|
|
find_loop_by_file() { losetup -j "$1" | awk -F: '{print $1}' | head -n1; }
|
|
ensure_loop() { local f="$1"; local l; l=$(find_loop_by_file "$f" || true); [[ -z "$l" ]] && l=$(losetup --find --show "$f"); echo "$l"; }
|
|
mapper_path() { echo "/dev/mapper/$1"; }
|
|
is_mapped() { [[ -e "$(mapper_path "$1")" ]]; }
|
|
|
|
mount_if_requested() { local dev="$1" mnt="${2:-}"; [[ -z "$mnt" ]] && return 0; mkdir -p "$mnt"; mount "$dev" "$mnt"; echo "$mnt"; }
|
|
umount_if_mounted() {
|
|
local dev="$1"
|
|
while read -r t; do [[ -n "$t" ]] && umount "$t"; done < <(findmnt -n -o TARGET "$dev" 2>/dev/null || true)
|
|
}
|
|
mkfs_for() {
|
|
local fs="$1" dev="$2"
|
|
case "$fs" in
|
|
ext4) mkfs.ext4 -F "$dev" ;;
|
|
xfs) mkfs.xfs -f "$dev" ;;
|
|
btrfs) mkfs.btrfs -f "$dev" ;;
|
|
*) die "Unsupported/unknown fs: $fs" ;;
|
|
esac
|
|
}
|
|
|
|
# ---------- actions (create/open/close/etc.) ----------
|
|
do_create() {
|
|
local file="" size="" fs="ext4" name="" mount_dir=""
|
|
local cipher="aes-xts-plain64" key_size="512" hash="sha512" pbkdf="argon2id"
|
|
local sparse=0 no_mkfs=0 keyfile=""
|
|
|
|
while (( $# )); do
|
|
set -- $(expand_kv "$@")
|
|
case "$1" in
|
|
--file) shift; file="$1" ;;
|
|
--size) shift; size="$1" ;;
|
|
--fs) shift; fs="$1" ;;
|
|
--name) shift; name="$1" ;;
|
|
--mount) shift; mount_dir="$1" ;;
|
|
--cipher) shift; cipher="$1" ;;
|
|
--key-size) shift; key_size="$1" ;;
|
|
--hash) shift; hash="$1" ;;
|
|
--pbkdf) shift; pbkdf="$1" ;;
|
|
--keyfile) shift; keyfile="$1" ;;
|
|
--sparse) sparse=1 ;;
|
|
--no-mkfs) no_mkfs=1 ;;
|
|
--help|-h) usage ;;
|
|
*) die "Unknown arg: $1" ;;
|
|
esac; shift || true
|
|
done
|
|
|
|
[[ -z "$file" || -z "$size" ]] && usage
|
|
[[ -z "$name" ]] && name="$(basename "${file%.*}")"
|
|
|
|
need cryptsetup; need losetup; need mount; need findmnt; as_root "$@"
|
|
|
|
banner "Creating image file ($size) at $file"
|
|
if (( sparse )); then
|
|
fallocate -l "$size" "$file" 2>/dev/null || dd if=/dev/zero of="$file" bs=1 count=0 seek="$size"
|
|
else
|
|
fallocate -l "$size" "$file" 2>/dev/null || dd if=/dev/zero of="$file" bs=1M count="${size%G}" seek=0
|
|
fi
|
|
|
|
banner "Attaching loop device"
|
|
local loop; loop=$(ensure_loop "$file"); echo "Loop: $loop"
|
|
|
|
banner "Initializing LUKS2"
|
|
local luks_args=(--type luks2 --cipher "$cipher" --key-size "$key_size" --hash "$hash" --pbkdf "$pbkdf")
|
|
if [[ -n "$keyfile" ]]; then
|
|
[[ -f "$keyfile" ]] || die "Keyfile not found: $keyfile"
|
|
cryptsetup luksFormat "${luks_args[@]}" --batch-mode --key-file "$keyfile" "$loop"
|
|
else
|
|
cryptsetup luksFormat "${luks_args[@]}" "$loop"
|
|
fi
|
|
|
|
banner "Opening mapper $name"
|
|
if [[ -n "$keyfile" ]]; then cryptsetup luksOpen "$loop" "$name" --key-file "$keyfile"
|
|
else cryptsetup luksOpen "$loop" "$name"; fi
|
|
|
|
local dev="/dev/mapper/$name"
|
|
if (( ! no_mkfs )); then banner "mkfs.$fs on $dev"; mkfs_for "$fs" "$dev"; fi
|
|
if [[ -n "$mount_dir" ]]; then banner "Mounting at $mount_dir"; mount_if_requested "$dev" "$mount_dir" >/dev/null; fi
|
|
|
|
echo "DONE. Loop=$loop Mapper=$dev"
|
|
}
|
|
|
|
do_open() {
|
|
local file="" name="" mount_dir="" keyfile=""
|
|
while (( $# )); do
|
|
set -- $(expand_kv "$@")
|
|
case "$1" in
|
|
--file) shift; file="$1" ;;
|
|
--name) shift; name="$1" ;;
|
|
--mount) shift; mount_dir="$1" ;;
|
|
--keyfile) shift; keyfile="$1" ;;
|
|
--help|-h) usage ;;
|
|
*) die "Unknown arg: $1" ;;
|
|
esac; shift || true
|
|
done
|
|
[[ -z "$file" ]] && usage
|
|
[[ -z "$name" ]] && name="$(basename "${file%.*}")"
|
|
need cryptsetup; need losetup; need mount; need findmnt; as_root "$@"
|
|
|
|
banner "Attach loop"
|
|
local loop; loop=$(ensure_loop "$file"); echo "Loop: $loop"
|
|
banner "Open LUKS -> $name"
|
|
if [[ -n "$keyfile" ]]; then cryptsetup luksOpen "$loop" "$name" --key-file "$keyfile"
|
|
else cryptsetup luksOpen "$loop" "$name"; fi
|
|
|
|
local dev="/dev/mapper/$name"
|
|
[[ -n "$mount_dir" ]] && { banner "Mounting $dev at $mount_dir"; mount_if_requested "$dev" "$mount_dir" >/dev/null; }
|
|
echo "Mapper: $dev"
|
|
}
|
|
|
|
do_close() {
|
|
local file="" name=""
|
|
while (( $# )); do
|
|
set -- $(expand_kv "$@")
|
|
case "$1" in
|
|
--file) shift; file="$1" ;;
|
|
--name) shift; name="$1" ;;
|
|
--help|-h) usage ;;
|
|
*) die "Unknown arg: $1" ;;
|
|
esac; shift || true
|
|
done
|
|
[[ -z "$file" ]] && usage
|
|
[[ -z "$name" ]] && name="$(basename "${file%.*}")"
|
|
need cryptsetup; need losetup; need findmnt; as_root "$@"
|
|
|
|
local loop; loop=$(find_loop_by_file "$file" || true)
|
|
local dev="/dev/mapper/$name"
|
|
|
|
if is_mapped "$name"; then
|
|
banner "Unmount mounts for $dev (if any)"
|
|
umount_if_mounted "$dev" || true
|
|
banner "cryptsetup luksClose $name"
|
|
cryptsetup luksClose "$name"
|
|
else
|
|
echo "Mapper not open: $name"
|
|
fi
|
|
|
|
if [[ -n "$loop" ]]; then banner "Detach loop $loop"; losetup -d "$loop"; else echo "No loop device for $file"; fi
|
|
echo "Closed."
|
|
}
|
|
|
|
do_status() { local name=""; while (( $# )); do case "$1" in --name) shift; name="$1";; *) usage;; esac; shift||true; done; [[ -z "$name" ]]&&usage; need cryptsetup; cryptsetup status "$name"||true; findmnt "/dev/mapper/$name"||true; }
|
|
do_dump() { local file=""; while (( $# )); do case "$1" in --file) shift; file="$1";; *) usage;; esac; shift||true; done; [[ -z "$file" ]]&&usage; need cryptsetup; cryptsetup luksDump "$file"; }
|
|
do_add_key() { local file="" keyfile="" existing=""; while (( $# )); do case "$1" in --file) shift; file="$1";; --keyfile) shift; keyfile="$1";; --existing-keyfile) shift; existing="$1";; *) usage;; esac; shift||true; done; [[ -z "$file"||-z "$keyfile" ]]&&usage; need cryptsetup; as_root "$@"; [[ -n "$existing" ]] && cryptsetup luksAddKey "$file" --key-file "$existing" "$keyfile" || cryptsetup luksAddKey "$file" "$keyfile"; echo "Key added."; }
|
|
do_header_backup() { local file="" out=""; while (( $# )); do case "$1" in --file) shift; file="$1";; --out) shift; out="$1";; *) usage;; esac; shift||true; done; [[ -z "$file"||-z "$out" ]]&&usage; need cryptsetup; as_root "$@"; cryptsetup luksHeaderBackup "$file" --header-backup-file "$out"; echo "Header -> $out"; }
|
|
do_header_restore() { local file="" in=""; while (( $# )); do case "$1" in --file) shift; file="$1";; --in) shift; in="$1";; *) usage;; esac; shift||true; done; [[ -z "$file"||-z "$in" ]]&&usage; need cryptsetup; as_root "$@"; cryptsetup luksHeaderRestore "$file" --header-backup-file "$in"; echo "Header restored from $in"; }
|
|
|
|
# ---------- VirtualBox ----------
|
|
do_launch_vbox() {
|
|
need VBoxManage
|
|
local file="" name="" keyfile="" vm="" controller="SATA Controller" port="0" start_vm=0
|
|
while (( $# )); do
|
|
set -- $(expand_kv "$@")
|
|
case "$1" in
|
|
--file) shift; file="$1" ;;
|
|
--name) shift; name="$1" ;;
|
|
--keyfile) shift; keyfile="$1" ;;
|
|
--vm) shift; vm="$1" ;;
|
|
--controller) shift; controller="$1" ;;
|
|
--port) shift; port="$1" ;;
|
|
--start) start_vm=1 ;;
|
|
--help|-h) usage ;;
|
|
*) die "Unknown arg: $1" ;;
|
|
esac; shift || true
|
|
done
|
|
[[ -z "$file" || -z "$vm" ]] && usage
|
|
[[ -z "$name" ]] && name="$(basename "${file%.*}")"
|
|
|
|
# Open (host-side decrypt) to /dev/mapper/NAME
|
|
"$0" open --file "$file" --name "$name" ${keyfile:+--keyfile "$keyfile"} >/dev/null
|
|
|
|
local mapper; mapper=$(mapper_path "$name")
|
|
[[ -b "$mapper" ]] || die "Mapper not found: $mapper"
|
|
|
|
# Create a raw VMDK wrapper pointing to the mapper
|
|
local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/luks-img"
|
|
mkdir -p "$cache_dir"
|
|
local wrapper="$cache_dir/${name}.vmdk"
|
|
|
|
banner "Creating raw VMDK wrapper: $wrapper -> $mapper"
|
|
# VBox needs read perms on the mapper; run VirtualBox as same user who owns it.
|
|
VBoxManage internalcommands createrawvmdk -filename "$wrapper" -rawdisk "$mapper" >/dev/null
|
|
|
|
banner "Attaching to VM: $vm (controller=$controller, port=$port)"
|
|
VBoxManage storageattach "$vm" \
|
|
--storagectl "$controller" \
|
|
--port "$port" --device 0 --type hdd --medium "$wrapper"
|
|
|
|
echo "VMDK wrapper: $wrapper"
|
|
if (( start_vm )); then
|
|
banner "Starting VM: $vm"
|
|
VBoxManage startvm "$vm" --type gui
|
|
else
|
|
echo "Attached. Start the VM when ready."
|
|
fi
|
|
echo "NOTE: Use 'detach-vbox' to safely remove and delete the wrapper."
|
|
}
|
|
|
|
do_detach_vbox() {
|
|
need VBoxManage
|
|
local vm="" controller="SATA Controller" port="0" wrapper=""
|
|
while (( $# )); do
|
|
set -- $(expand_kv "$@")
|
|
case "$1" in
|
|
--vm) shift; vm="$1" ;;
|
|
--controller) shift; controller="$1" ;;
|
|
--port) shift; port="$1" ;;
|
|
--wrapper) shift; wrapper="$1" ;;
|
|
--help|-h) usage ;;
|
|
*) die "Unknown arg: $1" ;;
|
|
esac; shift || true
|
|
done
|
|
[[ -z "$vm" ]] && usage
|
|
|
|
banner "Detaching disk from VM: $vm (controller=$controller, port=$port)"
|
|
VBoxManage storageattach "$vm" --storagectl "$controller" --port "$port" --device 0 --medium none || true
|
|
|
|
if [[ -n "$wrapper" && -f "$wrapper" ]]; then
|
|
banner "Deleting wrapper $wrapper"
|
|
rm -f -- "$wrapper"
|
|
else
|
|
echo "If you used launch-vbox, the wrapper is in \$XDG_CACHE_HOME/luks-img/<name>.vmdk"
|
|
fi
|
|
|
|
echo "Done. (This does NOT close your LUKS mapper—run 'close' if needed.)"
|
|
}
|
|
|
|
# ---------- libvirt / virt-manager ----------
|
|
do_attach_virt() {
|
|
need virsh
|
|
local file="" name="" keyfile="" vm="" target="vdb" bus="virtio" persistent=0
|
|
while (( $# )); do
|
|
set -- $(expand_kv "$@")
|
|
case "$1" in
|
|
--file) shift; file="$1" ;;
|
|
--name) shift; name="$1" ;;
|
|
--keyfile) shift; keyfile="$1" ;;
|
|
--vm) shift; vm="$1" ;;
|
|
--target) shift; target="$1" ;;
|
|
--bus) shift; bus="$1" ;;
|
|
--persistent) persistent=1 ;;
|
|
--help|-h) usage ;;
|
|
*) die "Unknown arg: $1" ;;
|
|
esac; shift || true
|
|
done
|
|
[[ -z "$file" || -z "$vm" ]] && usage
|
|
[[ -z "$name" ]] && name="$(basename "${file%.*}")"
|
|
|
|
"$0" open --file "$file" --name "$name" ${keyfile:+--keyfile "$keyfile"} >/dev/null
|
|
|
|
local mapper; mapper=$(mapper_path "$name")
|
|
[[ -b "$mapper" ]] || die "Mapper not found: $mapper"
|
|
|
|
banner "Attaching $mapper to libvirt VM '$vm' as $target (bus=$bus)"
|
|
if (( persistent )); then
|
|
virsh attach-disk "$vm" "$mapper" "$target" --targetbus "$bus" --persistent --driver qemu || die "virsh attach-disk failed"
|
|
else
|
|
virsh attach-disk "$vm" "$mapper" "$target" --targetbus "$bus" --driver qemu || die "virsh attach-disk failed"
|
|
fi
|
|
|
|
echo "Attached. (Virt-manager will show the new disk.)"
|
|
}
|
|
|
|
do_detach_virt() {
|
|
need virsh
|
|
local vm="" target="vdb" persistent=0
|
|
while (( $# )); do
|
|
set -- $(expand_kv "$@")
|
|
case "$1" in
|
|
--vm) shift; vm="$1" ;;
|
|
--target) shift; target="$1" ;;
|
|
--persistent) persistent=1 ;;
|
|
--help|-h) usage ;;
|
|
*) die "Unknown arg: $1" ;;
|
|
esac; shift || true
|
|
done
|
|
[[ -z "$vm" ]] && usage
|
|
|
|
banner "Detaching $target from libvirt VM '$vm'"
|
|
if (( persistent )); then
|
|
virsh detach-disk "$vm" "$target" --persistent || die "virsh detach-disk failed"
|
|
else
|
|
virsh detach-disk "$vm" "$target" || die "virsh detach-disk failed"
|
|
fi
|
|
|
|
echo "Detached. (This does NOT close your LUKS mapper—run 'close' if needed.)"
|
|
}
|
|
|
|
# ---------- main ----------
|
|
[[ $# -lt 1 ]] && usage
|
|
sub="$1"; shift || true
|
|
|
|
case "$sub" in
|
|
create) do_create "$@" ;;
|
|
open) do_open "$@" ;;
|
|
close) do_close "$@" ;;
|
|
status) do_status "$@" ;;
|
|
dump) do_dump "$@" ;;
|
|
add-key) do_add_key "$@" ;;
|
|
header-backup) do_header_backup "$@" ;;
|
|
header-restore) do_header_restore "$@" ;;
|
|
launch-vbox) do_launch_vbox "$@" ;;
|
|
detach-vbox) do_detach_vbox "$@" ;;
|
|
attach-virt) do_attach_virt "$@" ;;
|
|
detach-virt) do_detach_virt "$@" ;;
|
|
-h|--help|help) usage ;;
|
|
*) die "Unknown subcommand: $sub (use --help)" ;;
|
|
esac
|
|
|