commit
d138c434e4
@ -0,0 +1,382 @@ |
|||||||
|
#!/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 |
||||||
Loading…
Reference in new issue