From 4a2fa5d7e9dba8ae32df774509bcdd98e6df86d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC=20=D0=9A=D0=BE=D0=BA=D0=BE?= =?UTF-8?q?=D1=81?= Date: Thu, 29 Jan 2026 21:36:32 +0700 Subject: [PATCH] Initial commit --- sarcophagus | 289 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100755 sarcophagus diff --git a/sarcophagus b/sarcophagus new file mode 100755 index 0000000..7150cd1 --- /dev/null +++ b/sarcophagus @@ -0,0 +1,289 @@ +#!/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 &2 + [[ -n "$pw1" ]] && break + warn "Password cannot be empty" + done + + if [[ "${CONFIRM:-1}" == "1" && "$confirm" == "confirm" ]]; then + read -rsp "Confirm: " pw2 &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 < [size] # Create container (default: ${DEFAULT_SIZE}) + ${PROGNAME} mount # Mount container + ${PROGNAME} umount # Unmount container (use image path, NOT mountpoint) + ${PROGNAME} status # 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 [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 " + require_root + check_dependencies + mount_container "$1" "$2" + ;; + + umount|unmount) + [[ $# -ge 1 ]] || error "Usage: $PROGNAME umount " + require_root + check_dependencies + umount_container "$1" + ;; + + status) + [[ $# -ge 1 ]] || error "Usage: $PROGNAME status " + check_dependencies + status_container "$1" + ;; + + help) show_help ;; + *) error "Unknown command: $cmd" ;; + esac +} + +main "$@" +