718 lines
29 KiB
Bash
Executable File
718 lines
29 KiB
Bash
Executable File
#!/bin/bash
|
|
# /usr/local/lib/opsec/opsec-lib.sh — Shared OPSEC function library
|
|
# Sourced by all OPSEC scripts for config management and common operations
|
|
|
|
OPSEC_CONF="/etc/opsec/opsec.conf"
|
|
OPSEC_COUNTRY_CODES="/etc/opsec/country-codes.conf"
|
|
OPSEC_PROFILES_DIR="/etc/opsec/profiles"
|
|
OPSEC_STATE_FILE="/var/run/opsec-advanced.enabled"
|
|
OPSEC_BOOT_MARKER="/etc/opsec/boot-advanced.enabled"
|
|
|
|
# ─── COLOR OUTPUT ──────────────────────────────────────────────────────────────
|
|
opsec_green() { echo -e "\033[38;5;49m[+]\033[0m \033[38;5;49m$*\033[0m"; }
|
|
opsec_red() { echo -e "\033[38;5;196m[-]\033[0m \033[38;5;196m$*\033[0m"; }
|
|
opsec_yellow() { echo -e "\033[38;5;214m[*]\033[0m \033[38;5;214m$*\033[0m"; }
|
|
opsec_info() { echo -e "\033[38;5;39m[~]\033[0m \033[38;5;75m$*\033[0m"; }
|
|
opsec_cyan() { echo -e "\033[38;5;51m[>]\033[0m \033[38;5;51m$*\033[0m"; }
|
|
opsec_mag() { echo -e "\033[38;5;201m[*]\033[0m \033[38;5;201m$*\033[0m"; }
|
|
opsec_dim() { echo -e "\033[38;5;244m $*\033[0m"; }
|
|
opsec_hdr() { echo -e "\033[38;5;51m━━━ \033[38;5;201m$*\033[38;5;51m ━━━\033[0m"; }
|
|
|
|
# ─── CONFIG MANAGEMENT ─────────────────────────────────────────────────────────
|
|
|
|
opsec_load_config() {
|
|
if [ -f "$OPSEC_CONF" ]; then
|
|
# shellcheck disable=SC1090
|
|
. "$OPSEC_CONF"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
opsec_save_config() {
|
|
# Re-serialize all known keys back to config file
|
|
# Preserves comments and structure
|
|
local tmp
|
|
tmp=$(mktemp)
|
|
cat > "$tmp" << 'HEADER'
|
|
# /etc/opsec/opsec.conf — Central OPSEC Configuration
|
|
# Shell-sourceable KEY="value" format. All scripts source this file.
|
|
# Edit via: sudo opsec-config.sh (interactive TUI)
|
|
|
|
HEADER
|
|
|
|
cat >> "$tmp" << EOF
|
|
# ─── ACTIVE PROFILE ────────────────────────────────────────────────────────────
|
|
PROFILE_NAME="${PROFILE_NAME:-default}"
|
|
|
|
# ─── TOR SETTINGS ──────────────────────────────────────────────────────────────
|
|
TOR_CIRCUIT_ROTATION="${TOR_CIRCUIT_ROTATION:-30}"
|
|
TOR_BLACKLIST="${TOR_BLACKLIST:-}"
|
|
TOR_STRICT_NODES="${TOR_STRICT_NODES:-1}"
|
|
TOR_ISOLATION="${TOR_ISOLATION:-1}"
|
|
TOR_PADDING="${TOR_PADDING:-1}"
|
|
TOR_SOCKS_PORT="${TOR_SOCKS_PORT:-9050}"
|
|
TOR_DNS_PORT="${TOR_DNS_PORT:-5353}"
|
|
TOR_TRANS_PORT="${TOR_TRANS_PORT:-9040}"
|
|
TOR_NUM_GUARDS="${TOR_NUM_GUARDS:-3}"
|
|
TOR_SAFE_LOGGING="${TOR_SAFE_LOGGING:-1}"
|
|
|
|
# ─── DNS SETTINGS ──────────────────────────────────────────────────────────────
|
|
DNS_MODE="${DNS_MODE:-tor}"
|
|
DNS_CUSTOM_SERVERS="${DNS_CUSTOM_SERVERS:-}"
|
|
|
|
# ─── KILL SWITCH ───────────────────────────────────────────────────────────────
|
|
KILLSWITCH_ALLOW_DHCP="${KILLSWITCH_ALLOW_DHCP:-1}"
|
|
KILLSWITCH_ALLOW_OPENVPN="${KILLSWITCH_ALLOW_OPENVPN:-1}"
|
|
KILLSWITCH_ALLOW_WIREGUARD="${KILLSWITCH_ALLOW_WIREGUARD:-1}"
|
|
KILLSWITCH_EXTRA_PORTS="${KILLSWITCH_EXTRA_PORTS:-}"
|
|
|
|
# ─── MAC ADDRESS ───────────────────────────────────────────────────────────────
|
|
MAC_INTERFACES="${MAC_INTERFACES:-auto}"
|
|
MAC_VENDOR_SPOOF="${MAC_VENDOR_SPOOF:-}"
|
|
|
|
# ─── HOSTNAME ──────────────────────────────────────────────────────────────────
|
|
HOSTNAME_PATTERN="${HOSTNAME_PATTERN:-desktop}"
|
|
HOSTNAME_CUSTOM_PREFIX="${HOSTNAME_CUSTOM_PREFIX:-}"
|
|
|
|
# ─── SYSTEM HARDENING ──────────────────────────────────────────────────────────
|
|
HARDEN_IPV6="${HARDEN_IPV6:-1}"
|
|
HARDEN_SWAP="${HARDEN_SWAP:-1}"
|
|
HARDEN_CORE_DUMPS="${HARDEN_CORE_DUMPS:-1}"
|
|
HARDEN_CLIPBOARD_CLEAR="${HARDEN_CLIPBOARD_CLEAR:-0}"
|
|
HARDEN_SCREEN_LOCK="${HARDEN_SCREEN_LOCK:-1}"
|
|
HARDEN_SCREEN_LOCK_TIMEOUT="${HARDEN_SCREEN_LOCK_TIMEOUT:-300}"
|
|
HARDEN_TIMEZONE_SPOOF="${HARDEN_TIMEZONE_SPOOF:-0}"
|
|
HARDEN_TIMEZONE_VALUE="${HARDEN_TIMEZONE_VALUE:-UTC}"
|
|
HARDEN_LOCALE_SPOOF="${HARDEN_LOCALE_SPOOF:-0}"
|
|
HARDEN_LOCALE_VALUE="${HARDEN_LOCALE_VALUE:-en_US.UTF-8}"
|
|
|
|
# ─── LEAK PREVENTION ──────────────────────────────────────────────────────────
|
|
LEAK_WEBRTC_BLOCK="${LEAK_WEBRTC_BLOCK:-1}"
|
|
LEAK_USB_BLOCK="${LEAK_USB_BLOCK:-0}"
|
|
|
|
# ─── MONITORING ────────────────────────────────────────────────────────────────
|
|
MONITOR_PROCESSES="${MONITOR_PROCESSES:-0}"
|
|
MONITOR_LOG_ROTATION="${MONITOR_LOG_ROTATION:-1}"
|
|
LOG_ROTATION_HOURS="${LOG_ROTATION_HOURS:-4}"
|
|
|
|
# ─── TRAFFIC SHAPING ──────────────────────────────────────────────────────────
|
|
TRAFFIC_JITTER_ENABLED="${TRAFFIC_JITTER_ENABLED:-0}"
|
|
TRAFFIC_JITTER_MS="${TRAFFIC_JITTER_MS:-50}"
|
|
|
|
# ─── LEVEL TYPE ───────────────────────────────────────────────────────────────
|
|
LEVEL_TYPE="${LEVEL_TYPE:-standard}"
|
|
|
|
# ─── BASE STATE ───────────────────────────────────────────────────────────────
|
|
BASE_DNS="${BASE_DNS:-quad9}"
|
|
BASE_MAC_RANDOMIZE="${BASE_MAC_RANDOMIZE:-1}"
|
|
BASE_IPV6_DISABLE="${BASE_IPV6_DISABLE:-1}"
|
|
|
|
# ─── TOR BRIDGES ──────────────────────────────────────────────────────────────
|
|
TOR_BRIDGE_MODE="${TOR_BRIDGE_MODE:-off}"
|
|
TOR_BRIDGE_RELAY="${TOR_BRIDGE_RELAY:-}"
|
|
|
|
# ─── SECURE DELETION ──────────────────────────────────────────────────────────
|
|
WIPE_METHOD="${WIPE_METHOD:-auto}"
|
|
|
|
# ─── DEPLOYMENT LEVEL ─────────────────────────────────────────────────────────
|
|
DEPLOYMENT_LEVEL="${DEPLOYMENT_LEVEL:-bare-metal-standard}"
|
|
|
|
# ─── TERMINAL BANNER ──────────────────────────────────────────────────────────
|
|
OPSEC_BANNER="${OPSEC_BANNER:-compact}"
|
|
|
|
# ─── WIDGET THEME ────────────────────────────────────────────────────────────
|
|
WIDGET_THEME="${WIDGET_THEME:-default}"
|
|
EOF
|
|
mv "$tmp" "$OPSEC_CONF"
|
|
chmod 600 "$OPSEC_CONF"
|
|
}
|
|
|
|
opsec_set_value() {
|
|
local key="$1" val="$2"
|
|
if [ -z "$key" ]; then return 1; fi
|
|
# Validate key: must be a valid shell variable name (letters, digits, underscore, starts with letter/underscore)
|
|
if ! echo "$key" | grep -qE '^[A-Za-z_][A-Za-z0-9_]*$'; then
|
|
echo "[!] opsec_set_value: invalid key name '${key}'" >&2
|
|
return 1
|
|
fi
|
|
# Sanitize value: strip characters that could break shell quoting
|
|
val=$(printf '%s' "$val" | tr -d '`$\\\"'"'" | tr -cd '[:print:]')
|
|
opsec_load_config
|
|
eval "${key}=\"${val}\""
|
|
opsec_save_config
|
|
}
|
|
|
|
opsec_get_value() {
|
|
local key="$1"
|
|
# Validate key: must be a valid shell variable name
|
|
if ! echo "$key" | grep -qE '^[A-Za-z_][A-Za-z0-9_]*$'; then
|
|
echo "[!] opsec_get_value: invalid key name '${key}'" >&2
|
|
return 1
|
|
fi
|
|
opsec_load_config
|
|
eval "echo \"\${${key}:-}\""
|
|
}
|
|
|
|
# ─── TORRC GENERATION ──────────────────────────────────────────────────────────
|
|
|
|
opsec_generate_torrc() {
|
|
local _dbg="/run/opsec/debug.log"
|
|
echo "[$(date -Is)] [lib] opsec_generate_torrc called" >> "$_dbg" 2>/dev/null || true
|
|
|
|
opsec_load_config || return 1
|
|
|
|
local torrc="/etc/tor/torrc"
|
|
local socks_port="${TOR_SOCKS_PORT:-9050}"
|
|
local trans_port="${TOR_TRANS_PORT:-9040}"
|
|
local dns_port="${TOR_DNS_PORT:-5353}"
|
|
local rotation="${TOR_CIRCUIT_ROTATION:-30}"
|
|
local blacklist="${TOR_BLACKLIST:-}"
|
|
local strict="${TOR_STRICT_NODES:-1}"
|
|
local isolation="${TOR_ISOLATION:-1}"
|
|
local padding="${TOR_PADDING:-1}"
|
|
local guards="${TOR_NUM_GUARDS:-3}"
|
|
local safe_log="${TOR_SAFE_LOGGING:-1}"
|
|
echo "[$(date -Is)] [lib] socks=${socks_port} trans=${trans_port} dns=${dns_port} blacklist=${blacklist}" >> "$_dbg" 2>/dev/null || true
|
|
|
|
# Build SocksPort line with isolation flags
|
|
local socks_line="SocksPort ${socks_port}"
|
|
if [ "$isolation" = "1" ]; then
|
|
socks_line="${socks_line} IsolateDestAddr IsolateDestPort"
|
|
fi
|
|
|
|
# Build ExcludeExitNodes from blacklist
|
|
local exclude_line=""
|
|
if [ -n "$blacklist" ]; then
|
|
local formatted
|
|
formatted=$(echo "$blacklist" | sed 's/\([a-z][a-z]\)/{\1}/g; s/,/,/g')
|
|
exclude_line="ExcludeExitNodes ${formatted}"
|
|
fi
|
|
|
|
local bridge_mode="${TOR_BRIDGE_MODE:-off}"
|
|
local bridge_relay="${TOR_BRIDGE_RELAY:-}"
|
|
|
|
cat > "$torrc" << EOF
|
|
# Autogenerated — do not edit manually
|
|
# Regenerate via: opsec-config.sh --apply
|
|
|
|
# ─── SOCKS, TRANSPARENT PROXY & DNS ─────────────────────────────────────────
|
|
${socks_line}
|
|
TransPort ${trans_port}
|
|
DNSPort ${dns_port}
|
|
|
|
# ─── EXIT NODE EXCLUSION ────────────────────────────────────────────────────
|
|
${exclude_line}
|
|
StrictNodes ${strict}
|
|
|
|
# ─── CIRCUIT ROTATION ───────────────────────────────────────────────────────
|
|
MaxCircuitDirtiness ${rotation}
|
|
|
|
# ─── TRAFFIC PADDING ────────────────────────────────────────────────────────
|
|
ConnectionPadding ${padding}
|
|
|
|
# ─── SAFE LOGGING ────────────────────────────────────────────────────────────
|
|
SafeLogging ${safe_log}
|
|
Log notice file /run/tor/notices.log
|
|
|
|
# ─── ENTRY GUARDS ───────────────────────────────────────────────────────────
|
|
NumEntryGuards ${guards}
|
|
UseEntryGuards 1
|
|
|
|
EOF
|
|
|
|
# ─── PLUGGABLE TRANSPORTS (bridges) ──────────────────────────────────────
|
|
if [ "$bridge_mode" != "off" ] && [ "$bridge_mode" != "" ]; then
|
|
cat >> "$torrc" << 'BRIDGE_HEADER'
|
|
|
|
# ─── BRIDGE CONFIGURATION ────────────────────────────────────────────────────
|
|
UseBridges 1
|
|
BRIDGE_HEADER
|
|
|
|
case "$bridge_mode" in
|
|
obfs4)
|
|
echo "ClientTransportPlugin obfs4 exec /usr/bin/obfs4proxy" >> "$torrc"
|
|
;;
|
|
meek-azure)
|
|
echo "ClientTransportPlugin meek_lite exec /usr/bin/obfs4proxy" >> "$torrc"
|
|
;;
|
|
snowflake)
|
|
# snowflake-client location varies by distro
|
|
local sf_bin
|
|
sf_bin=$(command -v snowflake-client 2>/dev/null || echo "/usr/bin/snowflake-client")
|
|
echo "ClientTransportPlugin snowflake exec ${sf_bin}" >> "$torrc"
|
|
;;
|
|
esac
|
|
|
|
# Add user-specified bridge relay if provided
|
|
if [ -n "$bridge_relay" ]; then
|
|
echo "Bridge ${bridge_relay}" >> "$torrc"
|
|
else
|
|
# Default bridges for each transport type
|
|
case "$bridge_mode" in
|
|
meek-azure)
|
|
echo "Bridge meek_lite 192.0.2.18:80 BE776A53492E1E044A26F17306E1BC46A55A1625 url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com" >> "$torrc"
|
|
;;
|
|
snowflake)
|
|
echo "Bridge snowflake 192.0.2.3:80 2B280B23E1107BB62ABFC40DDCC8824814F80A72 fingerprint=2B280B23E1107BB62ABFC40DDCC8824814F80A72 url=https://snowflake-broker.torproject.net.global.prod.fastly.net/ front=foursquare.com ice=stun:stun.l.google.com:19302,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478 utls-imitate=hellorandomizedalpn" >> "$torrc"
|
|
;;
|
|
esac
|
|
fi
|
|
fi
|
|
|
|
chmod 644 "$torrc"
|
|
echo "[$(date -Is)] [lib] torrc written ($(wc -l < "$torrc") lines)" >> "$_dbg" 2>/dev/null || true
|
|
echo "[$(date -Is)] [lib] torrc contents:" >> "$_dbg" 2>/dev/null || true
|
|
cat "$torrc" >> "$_dbg" 2>/dev/null || true
|
|
echo "[$(date -Is)] [lib] --- end torrc ---" >> "$_dbg" 2>/dev/null || true
|
|
}
|
|
|
|
# ─── RESOLV.CONF GENERATION ───────────────────────────────────────────────────
|
|
|
|
opsec_generate_resolv() {
|
|
local _dbg="/run/opsec/debug.log"
|
|
echo "[$(date -Is)] [lib] opsec_generate_resolv called" >> "$_dbg" 2>/dev/null || true
|
|
|
|
opsec_load_config || return 1
|
|
|
|
local mode="${DNS_MODE:-tor}"
|
|
local resolv="/etc/resolv.conf"
|
|
echo "[$(date -Is)] [lib] DNS_MODE=${mode}" >> "$_dbg" 2>/dev/null || true
|
|
|
|
# Unlock if immutable
|
|
chattr -i "$resolv" 2>/dev/null || true
|
|
|
|
case "$mode" in
|
|
tor)
|
|
echo "nameserver 127.0.0.1" > "$resolv"
|
|
;;
|
|
quad9)
|
|
cat > "$resolv" << 'EOF'
|
|
nameserver 9.9.9.9
|
|
nameserver 149.112.112.112
|
|
EOF
|
|
;;
|
|
cloudflare)
|
|
cat > "$resolv" << 'EOF'
|
|
nameserver 1.1.1.1
|
|
nameserver 1.0.0.1
|
|
EOF
|
|
;;
|
|
doh)
|
|
# DNS-over-HTTPS via dnscrypt-proxy
|
|
# Ensure dnscrypt-proxy is running
|
|
if command -v dnscrypt-proxy >/dev/null 2>&1; then
|
|
# Deploy config if not present
|
|
if [ ! -f /etc/dnscrypt-proxy/dnscrypt-proxy.toml ] && [ -f /etc/opsec/dnscrypt-proxy.toml ]; then
|
|
mkdir -p /etc/dnscrypt-proxy
|
|
cp /etc/opsec/dnscrypt-proxy.toml /etc/dnscrypt-proxy/dnscrypt-proxy.toml
|
|
fi
|
|
mkdir -p /var/log/dnscrypt-proxy /var/cache/dnscrypt-proxy
|
|
# Stop systemd-resolved if it conflicts on :53
|
|
systemctl stop systemd-resolved 2>/dev/null || true
|
|
systemctl start dnscrypt-proxy 2>/dev/null || dnscrypt-proxy -config /etc/dnscrypt-proxy/dnscrypt-proxy.toml &
|
|
fi
|
|
cat > "$resolv" << 'EOF'
|
|
nameserver 127.0.0.53
|
|
EOF
|
|
;;
|
|
dot)
|
|
# DNS-over-TLS via systemd-resolved
|
|
cat > "$resolv" << 'EOF'
|
|
nameserver 127.0.0.53
|
|
options edns0 trust-ad
|
|
EOF
|
|
;;
|
|
custom)
|
|
if [ -n "$DNS_CUSTOM_SERVERS" ]; then
|
|
: > "$resolv"
|
|
local IFS=','
|
|
for server in $DNS_CUSTOM_SERVERS; do
|
|
echo "nameserver ${server}" >> "$resolv"
|
|
done
|
|
else
|
|
echo "nameserver 9.9.9.9" > "$resolv"
|
|
fi
|
|
;;
|
|
*)
|
|
echo "nameserver 9.9.9.9" > "$resolv"
|
|
;;
|
|
esac
|
|
|
|
# Lock if in advanced mode
|
|
if opsec_is_advanced; then
|
|
chattr +i "$resolv"
|
|
echo "[$(date -Is)] [lib] resolv.conf locked (chattr +i)" >> "$_dbg" 2>/dev/null || true
|
|
fi
|
|
echo "[$(date -Is)] [lib] resolv.conf contents: $(cat "$resolv" 2>/dev/null | tr '\n' ' ')" >> "$_dbg" 2>/dev/null || true
|
|
}
|
|
|
|
# ─── STATE CHECKS ─────────────────────────────────────────────────────────────
|
|
|
|
opsec_is_advanced() {
|
|
[ -f "$OPSEC_STATE_FILE" ]
|
|
}
|
|
|
|
opsec_is_boot_enabled() {
|
|
[ -f "$OPSEC_BOOT_MARKER" ]
|
|
}
|
|
|
|
# ─── PROFILE MANAGEMENT ───────────────────────────────────────────────────────
|
|
|
|
opsec_profile_save() {
|
|
local name="$1"
|
|
if [ -z "$name" ]; then return 1; fi
|
|
mkdir -p "$OPSEC_PROFILES_DIR"
|
|
cp "$OPSEC_CONF" "${OPSEC_PROFILES_DIR}/${name}.conf"
|
|
# Tag profile name inside the saved copy
|
|
sed -i "s/^PROFILE_NAME=.*/PROFILE_NAME=\"${name}\"/" "${OPSEC_PROFILES_DIR}/${name}.conf"
|
|
}
|
|
|
|
opsec_profile_load() {
|
|
local name="$1"
|
|
local profile="${OPSEC_PROFILES_DIR}/${name}.conf"
|
|
if [ ! -f "$profile" ]; then return 1; fi
|
|
cp "$profile" "$OPSEC_CONF"
|
|
# Update active profile name
|
|
sed -i "s/^PROFILE_NAME=.*/PROFILE_NAME=\"${name}\"/" "$OPSEC_CONF"
|
|
}
|
|
|
|
opsec_profile_list() {
|
|
if [ -d "$OPSEC_PROFILES_DIR" ]; then
|
|
find "$OPSEC_PROFILES_DIR" -name '*.conf' -printf '%f\n' | sed 's/\.conf$//'
|
|
fi
|
|
}
|
|
|
|
opsec_profile_delete() {
|
|
local name="$1"
|
|
rm -f "${OPSEC_PROFILES_DIR}/${name}.conf"
|
|
}
|
|
|
|
opsec_profile_export() {
|
|
local name="$1" dest="$2"
|
|
local profile="${OPSEC_PROFILES_DIR}/${name}.conf"
|
|
if [ ! -f "$profile" ]; then return 1; fi
|
|
cp "$profile" "$dest"
|
|
}
|
|
|
|
opsec_profile_import() {
|
|
local src="$1" name="$2"
|
|
if [ ! -f "$src" ]; then return 1; fi
|
|
mkdir -p "$OPSEC_PROFILES_DIR"
|
|
cp "$src" "${OPSEC_PROFILES_DIR}/${name}.conf"
|
|
}
|
|
|
|
# ─── COUNTRY CODE HELPERS ─────────────────────────────────────────────────────
|
|
|
|
opsec_load_country_presets() {
|
|
if [ -f "$OPSEC_COUNTRY_CODES" ]; then
|
|
# shellcheck disable=SC1090
|
|
. "$OPSEC_COUNTRY_CODES"
|
|
fi
|
|
}
|
|
|
|
opsec_get_preset() {
|
|
local preset="$1"
|
|
opsec_load_country_presets
|
|
case "$preset" in
|
|
5eyes|fiveeyes) echo "$FIVE_EYES" ;;
|
|
9eyes|nineeyes) echo "$NINE_EYES" ;;
|
|
14eyes|fourteeneyes) echo "$FOURTEEN_EYES" ;;
|
|
surveillance) echo "$SURVEILLANCE_STATES" ;;
|
|
max) echo "$MAX_EXCLUSION" ;;
|
|
*) echo "" ;;
|
|
esac
|
|
}
|
|
|
|
# ─── DEPLOYMENT LEVEL MANAGEMENT ─────────────────────────────────────────────
|
|
|
|
OPSEC_LEVELS_DIR="/etc/opsec/levels"
|
|
|
|
opsec_level_list() {
|
|
if [ -d "$OPSEC_LEVELS_DIR" ]; then
|
|
find "$OPSEC_LEVELS_DIR" -name '*.conf' -printf '%f\n' | sed 's/\.conf$//' | sort
|
|
fi
|
|
}
|
|
|
|
opsec_level_apply() {
|
|
local level="$1"
|
|
local level_file="${OPSEC_LEVELS_DIR}/${level}.conf"
|
|
if [ ! -f "$level_file" ]; then
|
|
opsec_red "Level '${level}' not found in ${OPSEC_LEVELS_DIR}"
|
|
return 1
|
|
fi
|
|
|
|
# Preserve current PROFILE_NAME before overwriting
|
|
opsec_load_config 2>/dev/null || true
|
|
local saved_profile="${PROFILE_NAME:-default}"
|
|
|
|
# Copy level preset over active config
|
|
cp "$level_file" "$OPSEC_CONF"
|
|
chmod 600 "$OPSEC_CONF"
|
|
|
|
# Restore profile name and ensure deployment level is tagged
|
|
opsec_load_config
|
|
PROFILE_NAME="$saved_profile"
|
|
DEPLOYMENT_LEVEL="$level"
|
|
opsec_save_config
|
|
}
|
|
|
|
# ─── SECURE DELETION ─────────────────────────────────────────────────────────
|
|
|
|
opsec_detect_storage_type() {
|
|
# Detect storage type for a given path
|
|
# Returns: hdd | ssd | luks | unknown
|
|
local target_path="${1:-/}"
|
|
local device
|
|
|
|
# Find the block device for the path
|
|
device=$(df -P "$target_path" 2>/dev/null | tail -1 | awk '{print $1}')
|
|
[ -z "$device" ] && echo "unknown" && return
|
|
|
|
# Check for LUKS
|
|
if command -v cryptsetup >/dev/null 2>&1; then
|
|
# Check if device is on a dm-crypt layer
|
|
local dm_name
|
|
dm_name=$(basename "$device" 2>/dev/null)
|
|
if [ -e "/sys/block/${dm_name}/dm/uuid" ] 2>/dev/null; then
|
|
local dm_uuid
|
|
dm_uuid=$(cat "/sys/block/${dm_name}/dm/uuid" 2>/dev/null)
|
|
if echo "$dm_uuid" | grep -qi "CRYPT-LUKS"; then
|
|
echo "luks"
|
|
return
|
|
fi
|
|
fi
|
|
# Also check via dmsetup
|
|
if dmsetup info "$device" 2>/dev/null | grep -qi "CRYPT-LUKS"; then
|
|
echo "luks"
|
|
return
|
|
fi
|
|
fi
|
|
|
|
# Resolve to physical disk (strip partition number, handle /dev/mapper)
|
|
local phys_disk
|
|
phys_disk=$(lsblk -ndo PKNAME "$device" 2>/dev/null | head -1)
|
|
[ -z "$phys_disk" ] && phys_disk=$(echo "$device" | sed 's/[0-9]*$//' | sed 's|^/dev/||')
|
|
|
|
# Check rotational flag (0 = SSD, 1 = HDD)
|
|
local rotational_file="/sys/block/${phys_disk}/queue/rotational"
|
|
if [ -f "$rotational_file" ]; then
|
|
local rotational
|
|
rotational=$(cat "$rotational_file" 2>/dev/null)
|
|
if [ "$rotational" = "0" ]; then
|
|
echo "ssd"
|
|
else
|
|
echo "hdd"
|
|
fi
|
|
return
|
|
fi
|
|
|
|
echo "unknown"
|
|
}
|
|
|
|
opsec_secure_delete() {
|
|
# SSD-aware secure file deletion
|
|
# Usage: opsec_secure_delete <file_or_dir> [method]
|
|
# method: auto (default) | shred | fstrim | luks
|
|
local target="$1"
|
|
local method="${2:-${WIPE_METHOD:-auto}}"
|
|
|
|
[ -z "$target" ] && return 1
|
|
|
|
if [ "$method" = "auto" ]; then
|
|
method=$(opsec_detect_storage_type "$target")
|
|
fi
|
|
|
|
case "$method" in
|
|
hdd|shred)
|
|
# Traditional shred for spinning disks
|
|
if [ -d "$target" ]; then
|
|
find "$target" -type f -exec shred -fuz -n 1 {} \; 2>/dev/null
|
|
rm -rf "$target" 2>/dev/null
|
|
elif [ -f "$target" ]; then
|
|
shred -fuz -n 1 "$target" 2>/dev/null
|
|
fi
|
|
;;
|
|
ssd|fstrim)
|
|
# For SSDs: overwrite with zeros, delete, then fstrim
|
|
# shred is ineffective on SSDs due to wear leveling
|
|
if [ -d "$target" ]; then
|
|
find "$target" -type f -exec dd if=/dev/zero of={} bs=4k count=1 conv=notrunc 2>/dev/null \;
|
|
find "$target" -type f -delete 2>/dev/null
|
|
rm -rf "$target" 2>/dev/null
|
|
elif [ -f "$target" ]; then
|
|
dd if=/dev/zero of="$target" bs=4k count=1 conv=notrunc 2>/dev/null
|
|
rm -f "$target" 2>/dev/null
|
|
fi
|
|
# Request TRIM/discard on the filesystem
|
|
local mount_point
|
|
mount_point=$(df -P "${target%/*}" 2>/dev/null | tail -1 | awk '{print $6}')
|
|
if [ -n "$mount_point" ]; then
|
|
fstrim "$mount_point" 2>/dev/null || true
|
|
fi
|
|
;;
|
|
luks)
|
|
# For LUKS: zero file + rely on LUKS key destroy for full wipe
|
|
if [ -d "$target" ]; then
|
|
find "$target" -type f -exec dd if=/dev/zero of={} bs=4k count=1 conv=notrunc 2>/dev/null \;
|
|
find "$target" -type f -delete 2>/dev/null
|
|
rm -rf "$target" 2>/dev/null
|
|
elif [ -f "$target" ]; then
|
|
dd if=/dev/zero of="$target" bs=4k count=1 conv=notrunc 2>/dev/null
|
|
rm -f "$target" 2>/dev/null
|
|
fi
|
|
# Note: full LUKS key destroy handled by emergency-wipe
|
|
;;
|
|
*)
|
|
# Fallback: basic shred
|
|
if [ -d "$target" ]; then
|
|
find "$target" -type f -exec shred -fuz -n 1 {} \; 2>/dev/null
|
|
rm -rf "$target" 2>/dev/null
|
|
elif [ -f "$target" ]; then
|
|
shred -fuz -n 1 "$target" 2>/dev/null
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ─── WIDGET THEME MANAGEMENT ─────────────────────────────────────────────────
|
|
|
|
OPSEC_THEMES_DIR="/etc/opsec/themes"
|
|
|
|
opsec_theme_list() {
|
|
if [ -d "$OPSEC_THEMES_DIR" ]; then
|
|
find "$OPSEC_THEMES_DIR" -name '*.theme' -printf '%f\n' | sed 's/\.theme$//' | sort
|
|
fi
|
|
}
|
|
|
|
opsec_generate_conky() {
|
|
opsec_load_config || return 1
|
|
|
|
local theme="${WIDGET_THEME:-default}"
|
|
local theme_file="${OPSEC_THEMES_DIR}/${theme}.theme"
|
|
|
|
if [ ! -f "$theme_file" ]; then
|
|
opsec_red "Theme '${theme}' not found at ${theme_file}"
|
|
return 1
|
|
fi
|
|
|
|
# Source theme to get CONKY_COLOR* and CONKY_BG values
|
|
# shellcheck disable=SC1090
|
|
. "$theme_file"
|
|
|
|
local conky_conf_name="conky-opsec-widget.conf"
|
|
|
|
# Find and regenerate conky config for each user with the widget installed
|
|
for home_dir in /home/*; do
|
|
local conky_conf="${home_dir}/.config/conky/${conky_conf_name}"
|
|
[ -f "$conky_conf" ] || continue
|
|
|
|
local owner
|
|
owner=$(stat -c '%U' "$conky_conf" 2>/dev/null) || continue
|
|
|
|
cat > "$conky_conf" << CONKYEOF
|
|
-- OPSEC Status Widget
|
|
-- Colors managed by theme system via opsec-config.sh
|
|
-- Theme: ${theme} (${THEME_LABEL:-Custom})
|
|
|
|
conky.config = {
|
|
-- Window settings
|
|
alignment = 'top_right',
|
|
gap_x = 15,
|
|
gap_y = 60,
|
|
minimum_width = 400,
|
|
minimum_height = 200,
|
|
maximum_width = 420,
|
|
|
|
-- Multi-monitor: run xrandr --listmonitors to find head number
|
|
xinerama_head = 0,
|
|
|
|
-- Window type
|
|
own_window = true,
|
|
own_window_type = 'desktop',
|
|
own_window_transparent = false,
|
|
own_window_argb_visual = true,
|
|
own_window_argb_value = 210,
|
|
own_window_colour = '${CONKY_BG:-0d1117}',
|
|
own_window_hints = 'undecorated,below,sticky,skip_taskbar,skip_pager',
|
|
|
|
-- Drawing
|
|
double_buffer = true,
|
|
draw_shades = true,
|
|
default_shade_color = '000000',
|
|
draw_outline = false,
|
|
draw_borders = true,
|
|
border_inner_margin = 12,
|
|
border_outer_margin = 4,
|
|
border_width = 1,
|
|
border_colour = '1b3a5c',
|
|
stippled_borders = 0,
|
|
|
|
-- Font
|
|
use_xft = true,
|
|
font = 'JetBrains Mono:size=10',
|
|
override_utf8_locale = true,
|
|
|
|
-- Colors — Theme: ${theme}
|
|
default_color = 'b0b0b0',
|
|
color0 = '${CONKY_COLOR0:-df2020}',
|
|
color1 = '${CONKY_COLOR1:-33ff33}',
|
|
color2 = '${CONKY_COLOR2:-3a8fd6}',
|
|
color3 = '${CONKY_COLOR3:-0d1117}',
|
|
color4 = '${CONKY_COLOR4:-1f6feb}',
|
|
color5 = '${CONKY_COLOR5:-58a6ff}',
|
|
color6 = '${CONKY_COLOR6:-79c0ff}',
|
|
color7 = '${CONKY_COLOR7:-c9d1d9}',
|
|
color8 = '${CONKY_COLOR8:-1f6feb}',
|
|
color9 = '${CONKY_COLOR9:-484f58}',
|
|
|
|
-- Update interval
|
|
update_interval = 3,
|
|
total_run_times = 0,
|
|
|
|
-- Misc
|
|
cpu_avg_samples = 2,
|
|
no_buffers = true,
|
|
text_buffer_size = 8192,
|
|
short_units = true,
|
|
};
|
|
|
|
conky.text = [[
|
|
\${execpi 5 ~/.config/conky/conky-opsec-status.sh}
|
|
]];
|
|
CONKYEOF
|
|
|
|
chown "$owner":"$owner" "$conky_conf"
|
|
chmod 644 "$conky_conf"
|
|
done
|
|
|
|
# Restart conky for all users running the opsec widget
|
|
pkill -f 'conky.*opsec' 2>/dev/null || true
|
|
sleep 1
|
|
|
|
for home_dir in /home/*; do
|
|
local conky_conf="${home_dir}/.config/conky/${conky_conf_name}"
|
|
[ -f "$conky_conf" ] || continue
|
|
|
|
local owner
|
|
owner=$(stat -c '%U' "$conky_conf" 2>/dev/null) || continue
|
|
local uid
|
|
uid=$(id -u "$owner" 2>/dev/null) || continue
|
|
|
|
# Relaunch conky as the file owner
|
|
su - "$owner" -c "DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${uid}/bus nohup conky -c '${conky_conf}' >/dev/null 2>&1 &" 2>/dev/null || true
|
|
done
|
|
}
|
|
|
|
# ─── INIT CHECK ────────────────────────────────────────────────────────────────
|
|
# Ensure /etc/opsec exists
|
|
_opsec_init_dirs() {
|
|
[ -d /etc/opsec ] || mkdir -p /etc/opsec
|
|
[ -d "$OPSEC_PROFILES_DIR" ] || mkdir -p "$OPSEC_PROFILES_DIR"
|
|
}
|
|
|
|
# Auto-init on source if running as root
|
|
if [ "$EUID" = "0" ] 2>/dev/null || [ "$(id -u)" = "0" ] 2>/dev/null; then
|
|
_opsec_init_dirs
|
|
fi
|