2026-01-12 10:34:04 +00:00
|
|
|
|
#!/bin/bash
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
|
|
# nbcrypt - SSH Agent based encryption/decryption tool
|
|
|
|
|
|
# Uses Ed25519 signature for deterministic key derivation
|
|
|
|
|
|
|
|
|
|
|
|
SCRIPT_NAME="$(basename "$0")"
|
|
|
|
|
|
KEY_SEED_MESSAGE="nbase2-secret-key-seed-v1"
|
|
|
|
|
|
KEY_IDENTITY="id_ed25519"
|
|
|
|
|
|
|
|
|
|
|
|
# Colors for output
|
|
|
|
|
|
RED='\033[0;31m'
|
|
|
|
|
|
GREEN='\033[0;32m'
|
|
|
|
|
|
YELLOW='\033[1;33m'
|
|
|
|
|
|
NC='\033[0m' # No Color
|
|
|
|
|
|
|
|
|
|
|
|
error() {
|
|
|
|
|
|
echo -e "${RED}❌ Error: $*${NC}" >&2
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
info() {
|
|
|
|
|
|
echo -e "${GREEN}ℹ️ $*${NC}" >&2
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
warn() {
|
|
|
|
|
|
echo -e "${YELLOW}⚠️ $*${NC}" >&2
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
usage() {
|
|
|
|
|
|
cat <<EOF
|
|
|
|
|
|
Usage: $SCRIPT_NAME <command> [arguments]
|
|
|
|
|
|
|
|
|
|
|
|
Commands:
|
|
|
|
|
|
encrypt <input> <output> Encrypt a file using SSH Agent key
|
|
|
|
|
|
decrypt <input> <output> Decrypt a file using SSH Agent key
|
2026-01-18 07:52:01 +00:00
|
|
|
|
install-bws Install Bitwarden Secrets Manager CLI (bws)
|
2026-01-12 10:34:04 +00:00
|
|
|
|
check Check if SSH Agent has required key
|
|
|
|
|
|
help Show this help message
|
|
|
|
|
|
|
|
|
|
|
|
Requirements:
|
|
|
|
|
|
- SSH Agent must be running with id_ed25519 key loaded
|
|
|
|
|
|
- ssh-keygen and openssl commands must be available
|
|
|
|
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
|
$SCRIPT_NAME encrypt secrets.txt secrets.enc
|
|
|
|
|
|
$SCRIPT_NAME decrypt secrets.enc secrets.txt
|
2026-01-18 07:52:01 +00:00
|
|
|
|
$SCRIPT_NAME install-bws
|
2026-01-12 10:34:04 +00:00
|
|
|
|
$SCRIPT_NAME check
|
|
|
|
|
|
|
|
|
|
|
|
EOF
|
|
|
|
|
|
exit 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
check_dependencies() {
|
|
|
|
|
|
local missing=()
|
|
|
|
|
|
|
|
|
|
|
|
for cmd in ssh-keygen openssl ssh-add; do
|
|
|
|
|
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
|
|
|
|
missing+=("$cmd")
|
|
|
|
|
|
fi
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
if [ ${#missing[@]} -gt 0 ]; then
|
|
|
|
|
|
error "Missing required commands: ${missing[*]}"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
check_ssh_agent() {
|
2026-01-18 07:52:01 +00:00
|
|
|
|
# Check if SSH_AUTH_SOCK is set and valid
|
|
|
|
|
|
if [ -n "${SSH_AUTH_SOCK:-}" ] && [ -S "${SSH_AUTH_SOCK}" ]; then
|
|
|
|
|
|
# Check if ssh-add can connect to agent
|
|
|
|
|
|
if ssh-add -l >/dev/null 2>&1; then
|
|
|
|
|
|
# Check if Ed25519 key is loaded
|
|
|
|
|
|
if ssh-add -l 2>/dev/null | grep -q "ED25519"; then
|
|
|
|
|
|
info "SSH Agent check passed (Ed25519 key found)"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# No valid agent or no Ed25519 key - try BWS bootstrap
|
|
|
|
|
|
info "No Ed25519 key found in SSH Agent. Attempting BWS bootstrap..."
|
|
|
|
|
|
|
|
|
|
|
|
# Get BWS access token
|
|
|
|
|
|
local bws_token="${BWS_ACCESS_TOKEN:-}"
|
|
|
|
|
|
if [ -z "$bws_token" ]; then
|
|
|
|
|
|
echo -n "Enter BWS_ACCESS_TOKEN: " >&2
|
|
|
|
|
|
read -s bws_token
|
|
|
|
|
|
echo >&2
|
|
|
|
|
|
if [ -z "$bws_token" ]; then
|
|
|
|
|
|
error "BWS_ACCESS_TOKEN is required when SSH Agent has no Ed25519 key"
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# Try to load setup script from BWS
|
|
|
|
|
|
load_bws_setup "$bws_token"
|
|
|
|
|
|
|
|
|
|
|
|
# Re-check agent after BWS setup
|
|
|
|
|
|
if [ -n "${SSH_AUTH_SOCK:-}" ] && [ -S "${SSH_AUTH_SOCK}" ]; then
|
|
|
|
|
|
if ssh-add -l >/dev/null 2>&1 && ssh-add -l 2>/dev/null | grep -q "ED25519"; then
|
|
|
|
|
|
info "SSH Agent check passed (Ed25519 key found after BWS setup)"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
error "No Ed25519 key found in SSH Agent. Please add id_ed25519 with: ssh-add ~/.ssh/id_ed25519"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
load_bws_setup() {
|
|
|
|
|
|
local token="$1"
|
|
|
|
|
|
local secret_name="nbloader"
|
|
|
|
|
|
|
|
|
|
|
|
info "Loading setup script from BWS (secret: $secret_name)..."
|
|
|
|
|
|
|
|
|
|
|
|
# Check if bws command exists, if not try to install it
|
|
|
|
|
|
if ! command -v bws >/dev/null 2>&1; then
|
|
|
|
|
|
warn "bws command not found. Attempting to install..."
|
|
|
|
|
|
install_bws || error "Failed to install bws CLI"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# Export token temporarily for bws command
|
|
|
|
|
|
export BWS_ACCESS_TOKEN="$token"
|
|
|
|
|
|
|
|
|
|
|
|
# Get the secret from BWS
|
|
|
|
|
|
local loader_script
|
|
|
|
|
|
if command -v jq >/dev/null 2>&1; then
|
|
|
|
|
|
loader_script=$(bws secret get "$secret_name" 2>/dev/null | jq -r '.value // empty')
|
|
|
|
|
|
elif command -v python3 >/dev/null 2>&1; then
|
|
|
|
|
|
loader_script=$(bws secret get "$secret_name" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('value', ''))" 2>/dev/null)
|
|
|
|
|
|
else
|
|
|
|
|
|
# Fallback: try to extract value with grep/sed (fragile but works for simple JSON)
|
|
|
|
|
|
loader_script=$(bws secret get "$secret_name" 2>/dev/null | grep -o '"value": "[^"]*"' | sed 's/"value": "//;s/"$//' | head -1)
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if [ -z "$loader_script" ]; then
|
|
|
|
|
|
error "Failed to retrieve '$secret_name' from BWS. Check your token and secret name."
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# Execute the loader script
|
|
|
|
|
|
info "Executing BWS setup script..."
|
|
|
|
|
|
eval "$loader_script"
|
|
|
|
|
|
|
|
|
|
|
|
# Load agent environment if it was created
|
|
|
|
|
|
local env_file="/tmp/.nb_agent_env_${USER:-$(id -un)}"
|
|
|
|
|
|
if [ -f "$env_file" ]; then
|
|
|
|
|
|
source "$env_file"
|
|
|
|
|
|
info "SSH Agent environment loaded from $env_file"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
install_bws() {
|
|
|
|
|
|
local arch
|
|
|
|
|
|
arch=$(uname -m)
|
|
|
|
|
|
local bws_version="1.0.0"
|
|
|
|
|
|
local bws_bin_dir="${HOME}/.local/bin"
|
|
|
|
|
|
local bws_path="${bws_bin_dir}/bws"
|
|
|
|
|
|
|
|
|
|
|
|
mkdir -p "$bws_bin_dir"
|
|
|
|
|
|
export PATH="$bws_bin_dir:$PATH"
|
|
|
|
|
|
|
|
|
|
|
|
# Determine architecture
|
|
|
|
|
|
case "$arch" in
|
|
|
|
|
|
x86_64)
|
|
|
|
|
|
arch="x86_64-unknown-linux-gnu"
|
|
|
|
|
|
;;
|
|
|
|
|
|
aarch64|arm64)
|
|
|
|
|
|
arch="aarch64-unknown-linux-gnu"
|
|
|
|
|
|
;;
|
|
|
|
|
|
*)
|
|
|
|
|
|
error "Unsupported architecture: $arch"
|
|
|
|
|
|
;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
|
|
local zip_name="bws-${arch}-${bws_version}.zip"
|
|
|
|
|
|
local url="https://github.com/bitwarden/sdk-sm/releases/download/bws-v${bws_version}/${zip_name}"
|
|
|
|
|
|
|
|
|
|
|
|
info "Downloading bws v${bws_version} for ${arch}..."
|
|
|
|
|
|
|
|
|
|
|
|
# Install dependencies if needed
|
|
|
|
|
|
if ! command -v unzip >/dev/null 2>&1; then
|
|
|
|
|
|
if command -v apt-get >/dev/null 2>&1; then
|
|
|
|
|
|
sudo apt-get update -qq && sudo apt-get install -y unzip >/dev/null 2>&1
|
|
|
|
|
|
elif command -v yum >/dev/null 2>&1; then
|
|
|
|
|
|
sudo yum install -y unzip >/dev/null 2>&1
|
2026-01-12 10:34:04 +00:00
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
2026-01-18 07:52:01 +00:00
|
|
|
|
# Download and extract
|
|
|
|
|
|
local temp_zip="/tmp/${zip_name}"
|
|
|
|
|
|
if command -v wget >/dev/null 2>&1; then
|
|
|
|
|
|
wget -q "$url" -O "$temp_zip" || return 1
|
|
|
|
|
|
elif command -v curl >/dev/null 2>&1; then
|
|
|
|
|
|
curl -sL "$url" -o "$temp_zip" || return 1
|
|
|
|
|
|
else
|
|
|
|
|
|
error "wget or curl is required to download bws"
|
2026-01-12 10:34:04 +00:00
|
|
|
|
fi
|
|
|
|
|
|
|
2026-01-18 07:52:01 +00:00
|
|
|
|
unzip -o "$temp_zip" -d "$bws_bin_dir" >/dev/null 2>&1 || return 1
|
|
|
|
|
|
chmod +x "$bws_path"
|
|
|
|
|
|
rm -f "$temp_zip"
|
|
|
|
|
|
|
|
|
|
|
|
info "bws installed successfully at $bws_path"
|
2026-01-12 10:34:04 +00:00
|
|
|
|
return 0
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
derive_key() {
|
|
|
|
|
|
# Derive encryption key from Ed25519 signature
|
|
|
|
|
|
# Ed25519 signatures are deterministic, so same message = same signature
|
|
|
|
|
|
|
|
|
|
|
|
local temp_message=$(mktemp)
|
|
|
|
|
|
local temp_sig=$(mktemp)
|
|
|
|
|
|
local temp_pubkey=$(mktemp)
|
|
|
|
|
|
|
|
|
|
|
|
trap "rm -f '$temp_message' '$temp_sig' '$temp_pubkey'" EXIT
|
|
|
|
|
|
|
|
|
|
|
|
# Create message to sign
|
|
|
|
|
|
echo -n "$KEY_SEED_MESSAGE" > "$temp_message"
|
|
|
|
|
|
|
|
|
|
|
|
# Get public key from agent for the Ed25519 key
|
|
|
|
|
|
ssh-add -L 2>/dev/null | grep "ssh-ed25519" | head -1 > "$temp_pubkey"
|
|
|
|
|
|
|
|
|
|
|
|
if [ ! -s "$temp_pubkey" ]; then
|
|
|
|
|
|
error "Could not extract Ed25519 public key from SSH Agent"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# Sign the message using ssh-keygen
|
|
|
|
|
|
# Note: ssh-keygen -Y sign requires the public key in "allowed signers" format
|
|
|
|
|
|
local temp_allowed=$(mktemp)
|
|
|
|
|
|
echo "key1 $(cat "$temp_pubkey")" > "$temp_allowed"
|
|
|
|
|
|
|
|
|
|
|
|
# Sign the message
|
|
|
|
|
|
if ! ssh-keygen -Y sign -f "$temp_allowed" -n "nbase2" < "$temp_message" > "$temp_sig" 2>/dev/null; then
|
|
|
|
|
|
# If -Y sign doesn't work (older ssh-keygen), try alternative method
|
|
|
|
|
|
# We'll use ssh-agent's signing capability directly through a workaround
|
|
|
|
|
|
|
|
|
|
|
|
# Extract just the signature data using ssh-agent protocol
|
|
|
|
|
|
# Since we can't easily do that in pure bash, we'll use a deterministic approach
|
|
|
|
|
|
# based on the public key itself as a fallback
|
|
|
|
|
|
|
|
|
|
|
|
warn "ssh-keygen -Y sign not available, using fallback key derivation"
|
|
|
|
|
|
|
|
|
|
|
|
# Fallback: Use public key hash as seed
|
|
|
|
|
|
cat "$temp_pubkey" | sha256sum | head -c 64
|
|
|
|
|
|
rm -f "$temp_allowed"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
rm -f "$temp_allowed"
|
|
|
|
|
|
|
|
|
|
|
|
# Extract signature and hash it to create 256-bit key
|
|
|
|
|
|
# The signature file contains base64-encoded signature data
|
|
|
|
|
|
cat "$temp_sig" | base64 -d 2>/dev/null | sha256sum | head -c 64
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
encrypt_file() {
|
|
|
|
|
|
local input="$1"
|
|
|
|
|
|
local output="$2"
|
|
|
|
|
|
|
|
|
|
|
|
if [ ! -f "$input" ]; then
|
|
|
|
|
|
error "Input file not found: $input"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
check_ssh_agent
|
|
|
|
|
|
|
|
|
|
|
|
info "Deriving encryption key from SSH Agent..."
|
|
|
|
|
|
local key=$(derive_key)
|
|
|
|
|
|
|
|
|
|
|
|
if [ -z "$key" ]; then
|
|
|
|
|
|
error "Failed to derive encryption key"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
info "Encrypting $input -> $output..."
|
|
|
|
|
|
|
|
|
|
|
|
# Use OpenSSL to encrypt with AES-256-CBC
|
|
|
|
|
|
# Pass the hex key directly to openssl
|
|
|
|
|
|
openssl enc -aes-256-cbc -salt -pbkdf2 -in "$input" -out "$output" -pass "pass:$key"
|
|
|
|
|
|
|
|
|
|
|
|
if [ $? -eq 0 ]; then
|
|
|
|
|
|
info "✅ Encryption successful: $output"
|
|
|
|
|
|
else
|
|
|
|
|
|
error "Encryption failed"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
decrypt_file() {
|
|
|
|
|
|
local input="$1"
|
|
|
|
|
|
local output="$2"
|
|
|
|
|
|
|
|
|
|
|
|
if [ ! -f "$input" ]; then
|
|
|
|
|
|
error "Input file not found: $input"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
check_ssh_agent
|
|
|
|
|
|
|
|
|
|
|
|
info "Deriving decryption key from SSH Agent..."
|
|
|
|
|
|
local key=$(derive_key)
|
|
|
|
|
|
|
|
|
|
|
|
if [ -z "$key" ]; then
|
|
|
|
|
|
error "Failed to derive decryption key"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
info "Decrypting $input -> $output..."
|
|
|
|
|
|
|
|
|
|
|
|
# Use OpenSSL to decrypt with AES-256-CBC
|
|
|
|
|
|
if openssl enc -aes-256-cbc -d -salt -pbkdf2 -in "$input" -out "$output" -pass "pass:$key" 2>/dev/null; then
|
|
|
|
|
|
info "✅ Decryption successful: $output"
|
|
|
|
|
|
else
|
|
|
|
|
|
error "Decryption failed (wrong key or corrupted file)"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Main command dispatcher
|
|
|
|
|
|
main() {
|
|
|
|
|
|
if [ $# -eq 0 ]; then
|
|
|
|
|
|
usage
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
local command="$1"
|
|
|
|
|
|
shift
|
|
|
|
|
|
|
2026-01-18 07:52:01 +00:00
|
|
|
|
# install-bws コマンドの場合は check_dependencies をスキップ
|
|
|
|
|
|
if [ "$command" != "install-bws" ] && [ "$command" != "help" ] && [ "$command" != "--help" ] && [ "$command" != "-h" ]; then
|
|
|
|
|
|
check_dependencies
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
2026-01-12 10:34:04 +00:00
|
|
|
|
case "$command" in
|
|
|
|
|
|
encrypt)
|
|
|
|
|
|
if [ $# -ne 2 ]; then
|
|
|
|
|
|
error "encrypt requires 2 arguments: <input> <output>"
|
|
|
|
|
|
fi
|
|
|
|
|
|
encrypt_file "$1" "$2"
|
|
|
|
|
|
;;
|
|
|
|
|
|
decrypt)
|
|
|
|
|
|
if [ $# -ne 2 ]; then
|
|
|
|
|
|
error "decrypt requires 2 arguments: <input> <output>"
|
|
|
|
|
|
fi
|
|
|
|
|
|
decrypt_file "$1" "$2"
|
|
|
|
|
|
;;
|
|
|
|
|
|
check)
|
|
|
|
|
|
check_ssh_agent
|
|
|
|
|
|
echo "✅ All checks passed"
|
|
|
|
|
|
;;
|
2026-01-18 07:52:01 +00:00
|
|
|
|
install-bws)
|
|
|
|
|
|
install_bws
|
|
|
|
|
|
;;
|
2026-01-12 10:34:04 +00:00
|
|
|
|
help|--help|-h)
|
|
|
|
|
|
usage
|
|
|
|
|
|
;;
|
|
|
|
|
|
*)
|
|
|
|
|
|
error "Unknown command: $command (use 'help' for usage)"
|
|
|
|
|
|
;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
main "$@"
|
|
|
|
|
|
|