Files
sarcophagus/sarcophagus
Артём Кокос 4a2fa5d7e9 Initial commit
2026-01-29 21:36:32 +07:00

290 lines
8.8 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Digital Sarcophagus - LUKS2 container manager for Ubuntu 20.04+
readonly PROGNAME="${0##*/}"
readonly VERSION="1.3.0"
readonly DEFAULT_SIZE="1G"
readonly DEFAULT_FS="ext4"
readonly PBKDF_MEMORY="1048576" # 1 GB RAM for Argon2id
readonly PBKDF_PARALLEL="4" # CPU threads for KDF
readonly CIPHER="aes-xts-plain64"
readonly KEY_SIZE="512"
readonly SECTOR_SIZE="4096"
log() { printf '%s [%s] %s\n' "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$PROGNAME" "$*" >&2; }
error() { log "ERROR: $*" >&2; exit 1; }
warn() { log "WARN: $*" >&2; }
debug() { [[ "${DEBUG:-0}" == "1" ]] && log "DEBUG: $*" >&2 || true; }
# -----------------------------------------------------------------------------
# Core utilities
# -----------------------------------------------------------------------------
check_dependencies() {
local cmd
for cmd in cryptsetup dd losetup mkfs.ext4 sync; do
command -v "$cmd" >/dev/null || error "Missing dependency: $cmd"
done
local ver
ver=$(cryptsetup --version | grep -oP '\d+\.\d+\.\d+' | head -1)
[[ "$ver" =~ ^2\. ]] || error "Need cryptsetup >= 2.0 (LUKS2). Found: $ver"
}
require_root() {
[[ $EUID -eq 0 ]] || error "Need root privileges (use sudo)"
}
validate_size() {
[[ "$1" =~ ^[0-9]+[KMGT]?$ ]] || error "Invalid size: $1 (use 100M, 1G, 10G)"
}
validate_path() {
local path="$1" action="$2"
case "$action" in
create)
[[ ! -e "$path" ]] || error "File exists: $path"
[[ -d "$(dirname "$path")" ]] || error "Parent directory missing"
;;
mount|umount|status)
[[ -f "$path" ]] || error "Container not found: $path"
;;
esac
}
read_password() {
local prompt="${1:-Password: }" confirm="${2:-}"
local pw1 pw2
while true; do
read -rsp "$prompt" pw1 </dev/tty || error "Failed to read password"
echo >&2
[[ -n "$pw1" ]] && break
warn "Password cannot be empty"
done
if [[ "${CONFIRM:-1}" == "1" && "$confirm" == "confirm" ]]; then
read -rsp "Confirm: " pw2 </dev/tty || error "Failed to read confirmation"
echo >&2
[[ "$pw1" == "$pw2" ]] || error "Passwords do not match"
fi
printf '%s' "$pw1"
}
get_loop_device() {
losetup --find --show --nooverlap "$1" 2>/dev/null || error "Cannot allocate loop device"
}
# -----------------------------------------------------------------------------
# LUKS operations
# -----------------------------------------------------------------------------
luks_format() {
local img="$1" size="$2"
log "Creating container: $img ($size)"
log "→ Allocating sparse file"
dd if=/dev/zero of="$img" bs=1M count=0 seek="${size%[KMGT]}" status=none
sync
log "→ Formatting LUKS2 (cipher=$CIPHER, pbkdf=argon2id)"
{
read_password "New password: " confirm
} | cryptsetup luksFormat \
--type luks2 \
--cipher "$CIPHER" \
--key-size "$KEY_SIZE" \
--sector-size "$SECTOR_SIZE" \
--pbkdf argon2id \
--pbkdf-memory "$PBKDF_MEMORY" \
--pbkdf-parallel "$PBKDF_PARALLEL" \
--force-password \
"$img" - || error "LUKS format failed"
log "✓ LUKS2 header created"
}
create_filesystem() {
local mapper="$1" fs="$2"
log "→ Creating $fs filesystem"
case "$fs" in
ext4)
mkfs.ext4 -q -E lazy_itable_init=0,lazy_journal_init=0 "/dev/mapper/$mapper"
tune2fs -m 0 "/dev/mapper/$mapper" >/dev/null 2>&1 || true
;;
*) error "Unsupported filesystem: $fs" ;;
esac
log "✓ Filesystem ready"
}
mount_container() {
local img="$1" mountpoint="$2"
local mapper_name="${3:-${img%.img}}"
mapper_name="${mapper_name##*/}" # basename without path
validate_path "$img" mount
[[ -d "$mountpoint" ]] || mkdir -p "$mountpoint"
mountpoint -q "$mountpoint" 2>/dev/null && error "Mountpoint in use: $mountpoint"
[[ ! -b "/dev/mapper/$mapper_name" ]] || error "Mapper already exists: $mapper_name"
local loopdev
loopdev=$(get_loop_device "$img")
debug "Loop device: $loopdev"
log "→ Opening LUKS container: $img → /dev/mapper/$mapper_name"
{
read_password "Password for $img: "
} | cryptsetup open --type luks "$loopdev" "$mapper_name" --key-file - || {
losetup -d "$loopdev" 2>/dev/null || true
error "LUKS unlock failed"
}
log "→ Mounting to $mountpoint"
mount -o noatime,nodiratime,nodev,nosuid "/dev/mapper/$mapper_name" "$mountpoint" || {
cryptsetup close "$mapper_name" 2>/dev/null || true
losetup -d "$loopdev" 2>/dev/null || true
error "Mount failed"
}
echo "LOOP_DEVICE=$loopdev" > "/run/sarcophagus-${mapper_name}.env" 2>/dev/null || true
log "✓ Mounted: $img$mountpoint"
}
umount_container() {
local img="$1"
local mapper_name="${img%.img}"
mapper_name="${mapper_name##*/}"
local envfile="/run/sarcophagus-${mapper_name}.env"
[[ -b "/dev/mapper/$mapper_name" ]] || error "Mapper not active: $mapper_name"
local mountpoint
mountpoint=$(findmnt -n -o TARGET "/dev/mapper/$mapper_name" 2>/dev/null)
[[ -n "$mountpoint" ]] || error "Not mounted. Close manually: cryptsetup close $mapper_name"
log "→ Unmounting from $mountpoint"
umount "$mountpoint" || error "Unmount failed (device busy?)"
log "→ Closing LUKS container"
cryptsetup close "$mapper_name" || error "cryptsetup close failed"
if [[ -f "$envfile" ]]; then
local loopdev
loopdev=$(grep '^LOOP_DEVICE=' "$envfile" | cut -d= -f2)
[[ -z "$loopdev" || ! "$loopdev" =~ ^/dev/loop ]] || losetup -d "$loopdev" 2>/dev/null
rm -f "$envfile"
fi
log "✓ Container closed"
}
status_container() {
local img="$1"
local mapper_name="${img%.img}"
mapper_name="${mapper_name##*/}"
echo "Container: $img"
echo "Mapper: $mapper_name"
echo -n "Active: "
[[ -b "/dev/mapper/$mapper_name" ]] && echo "yes" || echo "no"
if [[ -b "/dev/mapper/$mapper_name" ]]; then
echo "Mounts:"
mount | grep "/dev/mapper/$mapper_name" || echo " (none)"
fi
echo "Header:"
cryptsetup luksDump "$img" 2>/dev/null | grep -E "(Version|Cipher|PBKDF|Memory cost)" || echo " (unreadable)"
}
# -----------------------------------------------------------------------------
# CLI interface
# -----------------------------------------------------------------------------
show_help() {
cat <<EOF
${PROGNAME} v${VERSION} — LUKS2 container manager
Usage:
${PROGNAME} create <image.img> [size] # Create container (default: ${DEFAULT_SIZE})
${PROGNAME} mount <image.img> <mountpoint> # Mount container
${PROGNAME} umount <image.img> # Unmount container (use image path, NOT mountpoint)
${PROGNAME} status <image.img> # Show container state
${PROGNAME} help # This help
Examples:
sudo ${PROGNAME} create vault.img 10G
sudo ${PROGNAME} mount vault.img /mnt/vault
sudo ${PROGNAME} umount vault.img
Security:
• LUKS2 with Argon2id (1 GB RAM, 4 threads)
• AES-XTS 512-bit keys
• Passwords never logged or exposed in process list
• No backdoors — forgetting password = permanent data loss
Requirements:
Ubuntu 20.04+ with cryptsetup >= 2.0 (standard installation)
EOF
}
main() {
[[ $# -eq 0 ]] && { show_help; exit 1; }
local cmd="$1"; shift
case "$cmd" in
create)
[[ $# -ge 1 ]] || error "Usage: $PROGNAME create <image.img> [size]"
local img="$1" size="${2:-$DEFAULT_SIZE}"
validate_size "$size"
validate_path "$img" create
require_root
check_dependencies
luks_format "$img" "$size"
local mapper="${img%.img}"; mapper="${mapper##*/}"
{
read_password "Password for $img: "
} | cryptsetup open --type luks "$img" "$mapper" --key-file - || \
error "Cannot open container for FS creation"
create_filesystem "$mapper" "$DEFAULT_FS"
cryptsetup close "$mapper" || error "Cannot close container"
log "✓ Ready: $img (${size})"
;;
mount)
[[ $# -ge 2 ]] || error "Usage: $PROGNAME mount <image.img> <mountpoint>"
require_root
check_dependencies
mount_container "$1" "$2"
;;
umount|unmount)
[[ $# -ge 1 ]] || error "Usage: $PROGNAME umount <image.img>"
require_root
check_dependencies
umount_container "$1"
;;
status)
[[ $# -ge 1 ]] || error "Usage: $PROGNAME status <image.img>"
check_dependencies
status_container "$1"
;;
help) show_help ;;
*) error "Unknown command: $cmd" ;;
esac
}
main "$@"