init: initial bootstrap of pass sshkeys plugin
parent
986d07951d
commit
3a36a6c9b0
|
|
@ -0,0 +1,6 @@
|
||||||
|
install:
|
||||||
|
@echo "Installing pass-cli plugin (sshkeys.bash)..."
|
||||||
|
@mkdir -p ~/.password-store/.extensions/
|
||||||
|
@cp sshkeys.bash ~/.password-store/.extensions/
|
||||||
|
@chmod +x ~/.password-store/.extensions/sshkeys.bash
|
||||||
|
@echo "Installation complete. You can now use 'pass-sshkeys'."
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
class Record:
|
||||||
|
def __init__(self, name, url, username, password, extra, grouping, fav):
|
||||||
|
self.name_val = name
|
||||||
|
self.url = url
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.extra = extra
|
||||||
|
self.grouping = grouping
|
||||||
|
self.fav = fav
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
s = "lastpass/"
|
||||||
|
if self.grouping:
|
||||||
|
s += f"{self.grouping}/"
|
||||||
|
if self.name_val:
|
||||||
|
s += self.name_val
|
||||||
|
s = s.replace(" ", "_").replace("'", "")
|
||||||
|
return s
|
||||||
|
|
||||||
|
def to_str(self):
|
||||||
|
s = f"{self.password}\n---\n"
|
||||||
|
if self.grouping:
|
||||||
|
s += f"{self.grouping} / "
|
||||||
|
if self.name_val:
|
||||||
|
s += f"{self.name_val}\n"
|
||||||
|
if self.username:
|
||||||
|
s += f"username: {self.username}\n"
|
||||||
|
if self.password:
|
||||||
|
s += f"password: {self.password}\n"
|
||||||
|
if self.url and self.url != "http://sn":
|
||||||
|
s += f"url: {self.url}\n"
|
||||||
|
if self.extra:
|
||||||
|
s += f"{self.extra}\n"
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Import LastPass CSV export into pass")
|
||||||
|
parser.add_argument(
|
||||||
|
"-f", "--force", action="store_true", help="Overwrite existing records"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-d",
|
||||||
|
"--default",
|
||||||
|
metavar="GROUP",
|
||||||
|
default="",
|
||||||
|
help="Place uncategorised records into GROUP",
|
||||||
|
)
|
||||||
|
parser.add_argument("filename", help="Path to LastPass CSV file")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print(f"Reading '{args.filename}'...")
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
current_entry = []
|
||||||
|
entry_pattern = re.compile(r"^(http|ftp|ssh)")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(args.filename, "r") as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if entry_pattern.match(line):
|
||||||
|
if current_entry:
|
||||||
|
entries.append("\n".join(current_entry))
|
||||||
|
current_entry = []
|
||||||
|
current_entry.append(line)
|
||||||
|
if current_entry:
|
||||||
|
entries.append("\n".join(current_entry))
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"Couldn't find {args.filename}!")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"{len(entries)} records found!")
|
||||||
|
|
||||||
|
records = []
|
||||||
|
for entry in entries:
|
||||||
|
parts = entry.split(",")
|
||||||
|
url = parts[0]
|
||||||
|
username = parts[1] if len(parts) > 1 else ""
|
||||||
|
password = parts[2] if len(parts) > 2 else ""
|
||||||
|
fav = parts[-1] if len(parts) > 6 else ""
|
||||||
|
grouping = parts[-2] if len(parts) > 5 else args.default
|
||||||
|
name = parts[-3] if len(parts) > 4 else ""
|
||||||
|
extra = ",".join(parts[3:-4])[1:-1] if len(parts) > 7 else ""
|
||||||
|
|
||||||
|
records.append(Record(name, url, username, password, extra, grouping, fav))
|
||||||
|
|
||||||
|
print(f"Records parsed: {len(records)}")
|
||||||
|
|
||||||
|
successful = 0
|
||||||
|
errors = []
|
||||||
|
for record in records:
|
||||||
|
output_path = f"{record.name}.gpg"
|
||||||
|
if os.path.exists(output_path) and not args.force:
|
||||||
|
print(f"skipped {record.name}: already exists")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"Creating record {record.name}...", end="")
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
["pass", "insert", "-m", record.name], stdin=subprocess.PIPE, text=True
|
||||||
|
)
|
||||||
|
proc.communicate(input=record.to_str())
|
||||||
|
if proc.returncode == 0:
|
||||||
|
print(" done!")
|
||||||
|
successful += 1
|
||||||
|
else:
|
||||||
|
print(" error!")
|
||||||
|
errors.append(record)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" error! ({str(e)})")
|
||||||
|
errors.append(record)
|
||||||
|
|
||||||
|
print(f"{successful} records successfully imported!")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
print(f"There were {len(errors)} errors:")
|
||||||
|
error_names = [e.name for e in errors]
|
||||||
|
print(", ".join(error_names) + ".")
|
||||||
|
print(
|
||||||
|
"These probably occurred because an identically-named record already existed, or because there were multiple entries with the same name in the csv file."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# pass ssh extension for SSH key/config management
|
||||||
|
|
||||||
|
VERSION="0.2.0"
|
||||||
|
SSH_DIR="$HOME/.ssh"
|
||||||
|
CONFIG_FILE="$SSH_DIR/config"
|
||||||
|
PASS_DIR="$PASSWORD_STORE_DIR"
|
||||||
|
|
||||||
|
# Helper functions
|
||||||
|
die() { echo "Error: $*" >&2; exit 1; }
|
||||||
|
yesno() {
|
||||||
|
local answer
|
||||||
|
read -r -p "$1 [y/N] " answer
|
||||||
|
[[ "$answer" =~ [Yy] ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Process a Host block and store in pass
|
||||||
|
process_host_block() {
|
||||||
|
local hostname="$1"
|
||||||
|
local -a host_block=("${!2}")
|
||||||
|
|
||||||
|
# Extract IdentityFiles
|
||||||
|
local identity_files=()
|
||||||
|
for line in "${host_block[@]}"; do
|
||||||
|
if [[ "$line" =~ ^[Ii][Dd][Ee][Nn][Tt][Ii][Tt][Yy][Ff][Ii][Ll][Ee[[:space:]]+([^[:space:]]+) ]]; then
|
||||||
|
identity_files+=("${BASH_REMATCH[1]}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Process each IdentityFile
|
||||||
|
for identity_file in "${identity_files[@]}"; do
|
||||||
|
local expanded_path="${identity_file/#\~/$HOME}"
|
||||||
|
expanded_path=$(realpath -m "$expanded_path" 2>/dev/null)
|
||||||
|
|
||||||
|
# Resolve relative to SSH_DIR
|
||||||
|
[[ "$expanded_path" != "$SSH_DIR"/* ]] && expanded_path="$SSH_DIR/$identity_file"
|
||||||
|
|
||||||
|
if [[ -f "$expanded_path" ]]; then
|
||||||
|
local rel_path="${expanded_path#$SSH_DIR/}"
|
||||||
|
rel_path="${rel_path//../_dotdot_}"
|
||||||
|
local store_path="ssh/$hostname/$rel_path"
|
||||||
|
echo "Importing $expanded_path to $store_path"
|
||||||
|
pass insert --multiline "$store_path" < "$expanded_path" || die "Failed to insert $store_path"
|
||||||
|
else
|
||||||
|
echo "Skipping non-existent IdentityFile: $identity_file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Store Host block
|
||||||
|
local config_store="ssh/$hostname/config"
|
||||||
|
echo "Storing Host block in $config_store"
|
||||||
|
printf "%s\n" "${host_block[@]}" | pass insert --multiline "$config_store" >/dev/null || die "Failed to save config"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Import single host
|
||||||
|
cmd_import() {
|
||||||
|
local hostname="$1"
|
||||||
|
[[ -z "$hostname" ]] && die "Usage: pass ssh import <hostname>"
|
||||||
|
|
||||||
|
# Find Host block
|
||||||
|
local in_block=0
|
||||||
|
local host_block=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||||
|
if (( in_block )); then
|
||||||
|
if [[ "$line" =~ ^[Hh][Oo][Ss][Tt][[:space:]]+ ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
host_block+=("$line")
|
||||||
|
elif [[ "$line" =~ ^[Hh][Oo][Ss][Tt][[:space:]]+$hostname([[:space:]]+|$) ]]; then
|
||||||
|
in_block=1
|
||||||
|
host_block+=("$line")
|
||||||
|
fi
|
||||||
|
done < "$CONFIG_FILE"
|
||||||
|
|
||||||
|
(( ${#host_block[@]} )) || die "Host '$hostname' not found in $CONFIG_FILE"
|
||||||
|
process_host_block "$hostname" host_block[@]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Import all hosts from SSH config
|
||||||
|
cmd_import_all() {
|
||||||
|
echo "Parsing SSH config to find all Host blocks..."
|
||||||
|
|
||||||
|
# Parse all Host blocks
|
||||||
|
local current_host="" in_block=0
|
||||||
|
declare -a current_block all_hosts
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# Skip comments
|
||||||
|
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||||
|
|
||||||
|
if [[ "$line" =~ ^[Hh][Oo][Ss][Tt][[:space:]]+([^#]*) ]]; then
|
||||||
|
# New Host block
|
||||||
|
if (( in_block )); then
|
||||||
|
all_hosts+=("$current_host" "${current_block[@]}")
|
||||||
|
fi
|
||||||
|
current_host="${BASH_REMATCH[1]%% *}" # First hostname
|
||||||
|
current_block=("$line")
|
||||||
|
in_block=1
|
||||||
|
elif [[ "$line" =~ ^[Mm][Aa][Tt][Cc][Hh][[:space:]] ]] && (( in_block )); then
|
||||||
|
# End of Host block
|
||||||
|
all_hosts+=("$current_host" "${current_block[@]}")
|
||||||
|
current_host=""
|
||||||
|
current_block=()
|
||||||
|
in_block=0
|
||||||
|
elif (( in_block )); then
|
||||||
|
current_block+=("$line")
|
||||||
|
fi
|
||||||
|
done < "$CONFIG_FILE"
|
||||||
|
(( in_block )) && all_hosts+=("$current_host" "${current_block[@]}")
|
||||||
|
|
||||||
|
# Process all found Host blocks
|
||||||
|
local i=0
|
||||||
|
while (( i < ${#all_hosts[@]} )); do
|
||||||
|
local host="${all_hosts[i]}"
|
||||||
|
((i++))
|
||||||
|
local -a block=()
|
||||||
|
while (( i < ${#all_hosts[@]} )) && [[ ${all_hosts[i]} != "" ]]; do
|
||||||
|
block+=("${all_hosts[i]}")
|
||||||
|
((i++))
|
||||||
|
done
|
||||||
|
echo "Importing host: $host"
|
||||||
|
process_host_block "$host" block[@]
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export single host
|
||||||
|
cmd_export() {
|
||||||
|
local hostname="$1"
|
||||||
|
[[ -z "$hostname" ]] && die "Usage: pass ssh export <hostname>"
|
||||||
|
|
||||||
|
# Retrieve Host block
|
||||||
|
local config_store="ssh/$hostname/config"
|
||||||
|
local host_block
|
||||||
|
host_block=$(pass show "$config_store" 2>/dev/null) || die "No config found for $hostname"
|
||||||
|
|
||||||
|
# Check conflicts
|
||||||
|
local exported_patterns="${host_block%%$'\n'*}"
|
||||||
|
exported_patterns="${exported_patterns#Host }"
|
||||||
|
local conflict=0
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ ^[Hh][Oo][Ss][Tt][[:space:]]+ ]]; then
|
||||||
|
local existing_patterns="${line#Host }"
|
||||||
|
for pattern in $exported_patterns; do
|
||||||
|
[[ " $existing_patterns " == *" $pattern "* ]] && conflict=1
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
done < "$CONFIG_FILE"
|
||||||
|
|
||||||
|
if (( conflict )) && ! yesno "Overwrite conflicting Host entries?"; then
|
||||||
|
die "Export aborted"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Backup and merge config
|
||||||
|
local backup="${CONFIG_FILE}.bak.$(date +%s)"
|
||||||
|
cp "$CONFIG_FILE" "$backup" || die "Failed to backup config"
|
||||||
|
awk -v patterns="$exported_patterns" '
|
||||||
|
BEGIN { in_block=0; delete_lines=0 }
|
||||||
|
/^[Hh][Oo][Ss][Tt][[:space:]]+/ {
|
||||||
|
if (in_block) { in_block=0 }
|
||||||
|
split($0, parts, /[[:space:]]+/)
|
||||||
|
for (i=2; i<=NF; i++) {
|
||||||
|
for (p in patterns_arr) {
|
||||||
|
if (parts[i] == patterns_arr[p]) {
|
||||||
|
delete_lines=1
|
||||||
|
in_block=1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
in_block { next }
|
||||||
|
delete_lines { delete_lines=0; next }
|
||||||
|
{ print }
|
||||||
|
' "$backup" > "$CONFIG_FILE" || die "Failed to remove conflicts"
|
||||||
|
echo "$host_block" >> "$CONFIG_FILE"
|
||||||
|
|
||||||
|
# Export keys
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ "$line" =~ ^[Ii][Dd][Ee][Nn][Tt][Ii][Tt][Yy][Ff][Ii][Ll][Ee[[:space:]]+([^[:space:]]+) ]] || continue
|
||||||
|
local identity_file="${BASH_REMATCH[1]}"
|
||||||
|
local expanded_path="${identity_file/#\~/$HOME}"
|
||||||
|
expanded_path=$(realpath -m "$expanded_path")
|
||||||
|
[[ "$expanded_path" != "$SSH_DIR"/* ]] && expanded_path="$SSH_DIR/$identity_file"
|
||||||
|
local rel_path="${expanded_path#$SSH_DIR/}"
|
||||||
|
rel_path="${rel_path//../_dotdot_}"
|
||||||
|
local store_path="ssh/$hostname/$rel_path"
|
||||||
|
|
||||||
|
if [[ -f "$expanded_path" ]] && ! yesno "Overwrite $expanded_path?"; then
|
||||||
|
echo "Skipping $expanded_path"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
pass show "$store_path" > "$expanded_path" || echo "Warning: $store_path missing"
|
||||||
|
chmod 600 "$expanded_path"
|
||||||
|
done <<< "$host_block"
|
||||||
|
|
||||||
|
echo "Exported $hostname. Backup: $backup"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export all hosts
|
||||||
|
cmd_export_all() {
|
||||||
|
# Find all imported hosts
|
||||||
|
local hostname
|
||||||
|
while IFS= read -r -d '' hostname; do
|
||||||
|
hostname="${hostname#ssh/}"
|
||||||
|
hostname="${hostname%/}"
|
||||||
|
echo "Exporting host: $hostname"
|
||||||
|
cmd_export "$hostname"
|
||||||
|
done < <(find "$PASS_DIR/ssh" -mindepth 1 -maxdepth 1 -type d -printf '%P\0' 2>/dev/null)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main handler
|
||||||
|
case "$1" in
|
||||||
|
import) shift; cmd_import "$@" ;;
|
||||||
|
import-all) shift; cmd_import_all ;;
|
||||||
|
export) shift; cmd_export "$@" ;;
|
||||||
|
export-all) shift; cmd_export_all ;;
|
||||||
|
*) die "Usage: pass ssh import|import-all|export|export-all [hostname]" ;;
|
||||||
|
esac
|
||||||
|
|
@ -0,0 +1,231 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# pass ssh extension for importing/exporting SSH keys and configs
|
||||||
|
|
||||||
|
VERSION="0.1.0"
|
||||||
|
SSH_DIR="$HOME/.ssh"
|
||||||
|
CONFIG_FILE="$SSH_DIR/config"
|
||||||
|
PASS_DIR="$PASSWORD_STORE_DIR"
|
||||||
|
|
||||||
|
# Helper functions
|
||||||
|
die() { echo "Error: $*" >&2; exit 1; }
|
||||||
|
yesno() {
|
||||||
|
local answer
|
||||||
|
read -r -p "$1 [y/N] " answer
|
||||||
|
[[ "$answer" =~ [Yy] ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Import SSH keys and config into pass
|
||||||
|
cmd_import() {
|
||||||
|
local hostname="$1"
|
||||||
|
[[ -z "$hostname" ]] && die "Usage: pass ssh import <hostname>"
|
||||||
|
|
||||||
|
# Find Host block in SSH config
|
||||||
|
local in_block=0
|
||||||
|
local host_block=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ ^[[:space:]]*# ]]; then
|
||||||
|
continue # Skip comments
|
||||||
|
fi
|
||||||
|
if (( in_block )); then
|
||||||
|
if [[ "$line" =~ ^[Hh][Oo][Ss][Tt][[:space:]]+ ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
host_block+=("$line")
|
||||||
|
else
|
||||||
|
if [[ "$line" =~ ^[Hh][Oo][Ss][Tt][[:space:]]+$hostname([[:space:]]+|$) ]]; then
|
||||||
|
in_block=1
|
||||||
|
host_block+=("$line")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < "$CONFIG_FILE"
|
||||||
|
|
||||||
|
(( ${#host_block[@]} )) || die "Host '$hostname' not found in $CONFIG_FILE"
|
||||||
|
|
||||||
|
# Extract IdentityFiles
|
||||||
|
local identity_files=()
|
||||||
|
for line in "${host_block[@]}"; do
|
||||||
|
if [[ "$line" =~ ^[Ii][Dd][Ee][Nn][Tt][Ii][Tt][Yy][Ff][Ii][Ll][Ee][[:space:]]+([^[:space:]]+) ]]; then
|
||||||
|
identity_files+=("${BASH_REMATCH[1]}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Process each IdentityFile
|
||||||
|
for identity_file in "${identity_files[@]}"; do
|
||||||
|
# Expand path
|
||||||
|
local expanded_path="${identity_file/#\~/$HOME}"
|
||||||
|
expanded_path=$(realpath -m "$expanded_path" 2>/dev/null)
|
||||||
|
|
||||||
|
# Resolve relative to SSH_DIR
|
||||||
|
if [[ "$expanded_path" != "$SSH_DIR"/* ]]; then
|
||||||
|
expanded_path="$SSH_DIR/$identity_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if private key exists
|
||||||
|
if [[ -f "$expanded_path" ]]; then
|
||||||
|
# Determine store path
|
||||||
|
local rel_path="${expanded_path#$SSH_DIR/}"
|
||||||
|
rel_path="${rel_path//../_dotdot_}" # Sanitize ..
|
||||||
|
|
||||||
|
local store_path="ssh/$hostname/$rel_path"
|
||||||
|
echo "Importing $expanded_path to $store_path"
|
||||||
|
|
||||||
|
# Insert into pass
|
||||||
|
pass insert --multiline "$store_path" < "$expanded_path" || die "Failed to insert $store_path"
|
||||||
|
else
|
||||||
|
echo "Skipping non-existent IdentityFile: $identity_file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Save Host block
|
||||||
|
local config_store="ssh/$hostname/config"
|
||||||
|
echo "Storing Host block in $config_store"
|
||||||
|
printf "%s\n" "${host_block[@]}" | pass insert --multiline "$config_store" >/dev/null || die "Failed to save config"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export SSH keys and config from pass
|
||||||
|
cmd_export() {
|
||||||
|
local hostname="$1"
|
||||||
|
[[ -z "$hostname" ]] && die "Usage: pass ssh export <hostname>"
|
||||||
|
|
||||||
|
# Retrieve Host block
|
||||||
|
local config_store="ssh/$hostname/config"
|
||||||
|
local host_block
|
||||||
|
host_block=$(pass show "$config_store" 2>/dev/null) || die "No config found for $hostname"
|
||||||
|
|
||||||
|
# Check existing Host entries
|
||||||
|
local existing_patterns=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ ^[Hh][Oo][Ss][Tt][[:space:]]+([^#]+) ]]; then
|
||||||
|
existing_patterns+=("${BASH_REMATCH[1]}")
|
||||||
|
fi
|
||||||
|
done < "$CONFIG_FILE"
|
||||||
|
|
||||||
|
# Check if exported Host patterns exist
|
||||||
|
local exported_patterns
|
||||||
|
if [[ "$host_block" =~ ^[Hh][Oo][Ss][Tt][[:space:]]+([^[:space:]#]+) ]]; then
|
||||||
|
exported_patterns="${BASH_REMATCH[1]}"
|
||||||
|
else
|
||||||
|
die "Invalid Host block in $config_store"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for conflicts
|
||||||
|
local conflict=0
|
||||||
|
for pattern in $exported_patterns; do
|
||||||
|
for existing in "${existing_patterns[@]}"; do
|
||||||
|
if [[ " $existing " == *" $pattern "* ]]; then
|
||||||
|
echo "Conflict: Host pattern '$pattern' exists in $CONFIG_FILE"
|
||||||
|
conflict=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( conflict )) && ! yesno "Overwrite conflicting Host entries?"; then
|
||||||
|
die "Export aborted"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Backup original config
|
||||||
|
local backup="${CONFIG_FILE}.bak.$(date +%s)"
|
||||||
|
cp "$CONFIG_FILE" "$backup" || die "Failed to backup config"
|
||||||
|
|
||||||
|
# Remove conflicting Host blocks
|
||||||
|
awk -v patterns="$exported_patterns" '
|
||||||
|
BEGIN { in_block=0; delete_lines=0 }
|
||||||
|
/^[Hh][Oo][Ss][Tt][[:space:]]+/ {
|
||||||
|
if (in_block) { in_block=0 }
|
||||||
|
split($0, parts, /[[:space:]]+/)
|
||||||
|
for (i=2; i<=NF; i++) {
|
||||||
|
for (p in patterns_arr) {
|
||||||
|
if (parts[i] == patterns_arr[p]) {
|
||||||
|
delete_lines=1
|
||||||
|
in_block=1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
in_block { next }
|
||||||
|
delete_lines { delete_lines=0; next }
|
||||||
|
{ print }
|
||||||
|
' "$backup" > "$CONFIG_FILE" || die "Failed to remove conflicts"
|
||||||
|
|
||||||
|
# Append new Host block
|
||||||
|
echo "Appending Host block for $hostname to $CONFIG_FILE"
|
||||||
|
echo "$host_block" >> "$CONFIG_FILE"
|
||||||
|
|
||||||
|
# Export IdentityFiles
|
||||||
|
local identity_files=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ ^[Ii][Dd][Ee][Nn][Tt][Ii][Tt][Yy][Ff][Ii][Ll][Ee][[:space:]]+([^[:space:]]+) ]]; then
|
||||||
|
identity_files+=("${BASH_REMATCH[1]}")
|
||||||
|
fi
|
||||||
|
done <<< "$host_block"
|
||||||
|
|
||||||
|
for identity_file in "${identity_files[@]}"; do
|
||||||
|
local expanded_path="${identity_file/#\~/$HOME}"
|
||||||
|
expanded_path=$(realpath -m "$expanded_path")
|
||||||
|
|
||||||
|
# Resolve relative to SSH_DIR
|
||||||
|
if [[ "$expanded_path" != "$SSH_DIR"/* ]]; then
|
||||||
|
expanded_path="$SSH_DIR/$identity_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local rel_path="${expanded_path#$SSH_DIR/}"
|
||||||
|
rel_path="${rel_path//../_dotdot_}"
|
||||||
|
local store_path="ssh/$hostname/$rel_path"
|
||||||
|
|
||||||
|
if ! pass show "$store_path" >/dev/null 2>&1; then
|
||||||
|
echo "Warning: $store_path not found in pass"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$expanded_path" ]] && ! yesno "Overwrite $expanded_path?"; then
|
||||||
|
echo "Skipping $expanded_path"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Exporting $store_path to $expanded_path"
|
||||||
|
mkdir -p "$(dirname "$expanded_path")"
|
||||||
|
pass show "$store_path" > "$expanded_path"
|
||||||
|
chmod 600 "$expanded_path"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Export complete. Original config backed up to $backup"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Bulk operations
|
||||||
|
cmd_import_all() {
|
||||||
|
echo "Importing all hosts from $CONFIG_FILE"
|
||||||
|
local hostname=""
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ ^[Hh][Oo][Ss][Tt][[:space:]]+([^#[:space:]]+) ]]; then
|
||||||
|
hostname="${BASH_REMATCH[1]}"
|
||||||
|
echo "Importing host: $hostname"
|
||||||
|
cmd_import "$hostname" || echo "Failed to import $hostname"
|
||||||
|
fi
|
||||||
|
done < "$CONFIG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_export_all() {
|
||||||
|
echo "Exporting all hosts to $CONFIG_FILE"
|
||||||
|
local hosts=()
|
||||||
|
while IFS= read -r -d '' path; do
|
||||||
|
if [[ "$path" =~ ^ssh/([^/]+)/config ]]; then
|
||||||
|
hosts+=("${BASH_REMATCH[1]}")
|
||||||
|
fi
|
||||||
|
done < <(pass ls ssh | grep -F /config | tr '\n' '\0')
|
||||||
|
|
||||||
|
for host in "${hosts[@]}"; do
|
||||||
|
echo "Exporting host: $host"
|
||||||
|
cmd_export "$host" || echo "Failed to export $host"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main command handler
|
||||||
|
case "$1" in
|
||||||
|
import) shift; cmd_import "$@" ;;
|
||||||
|
export) shift; cmd_export "$@" ;;
|
||||||
|
import-all) shift; cmd_import_all "$@" ;;
|
||||||
|
export-all) shift; cmd_export_all "$@" ;;
|
||||||
|
*) die "Usage: pass ssh import|export|import-all|export-all <hostname>" ;;
|
||||||
|
esac
|
||||||
|
|
@ -0,0 +1,235 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# pass ssh extension for importing/exporting SSH keys and configs
|
||||||
|
|
||||||
|
VERSION="0.1.0"
|
||||||
|
SSH_DIR="$HOME/.ssh"
|
||||||
|
CONFIG_FILE="$SSH_DIR/config"
|
||||||
|
PASS_DIR="$PASSWORD_STORE_DIR"
|
||||||
|
|
||||||
|
# Helper functions
|
||||||
|
die() { echo "Error: $*" >&2; exit 1; }
|
||||||
|
yesno() {
|
||||||
|
local answer
|
||||||
|
read -r -p "$1 [y/N] " answer
|
||||||
|
[[ "$answer" =~ [Yy] ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Import SSH keys and config into pass
|
||||||
|
cmd_import() {
|
||||||
|
local hostname="$1"
|
||||||
|
[[ -z "$hostname" ]] && die "Usage: pass ssh import <hostname>"
|
||||||
|
|
||||||
|
# Find Host block in SSH config
|
||||||
|
local in_block=0
|
||||||
|
local host_block=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ ^[[:space:]]*# ]]; then
|
||||||
|
continue # Skip comments
|
||||||
|
fi
|
||||||
|
if (( in_block )); then
|
||||||
|
if [[ "$line" =~ ^[Hh][Oo][Ss][Tt][[:space:]]+ ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
host_block+=("$line")
|
||||||
|
else
|
||||||
|
if [[ "$line" =~ ^[Hh][Oo][Ss][Tt][[:space:]]+$hostname([[:space:]]+|$) ]]; then
|
||||||
|
in_block=1
|
||||||
|
host_block+=("$line")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < "$CONFIG_FILE"
|
||||||
|
|
||||||
|
(( ${#host_block[@]} )) || die "Host '$hostname' not found in $CONFIG_FILE"
|
||||||
|
|
||||||
|
# Extract IdentityFiles and paths
|
||||||
|
local identity_files=()
|
||||||
|
local identity_paths=()
|
||||||
|
for line in "${host_block[@]}"; do
|
||||||
|
if [[ "$line" =~ ^[Ii][Dd][Ee][Nn][Tt][Ii][Tt][Yy][Ff][Ii][Ll][Ee][[:space:]]+([^[:space:]]+) ]]; then
|
||||||
|
identity_file="${BASH_REMATCH[1]}"
|
||||||
|
identity_files+=("$identity_file")
|
||||||
|
|
||||||
|
# Store original path relative to SSH_DIR
|
||||||
|
if [[ "$identity_file" == "$SSH_DIR"/* ]]; then
|
||||||
|
rel_path="${identity_file#$SSH_DIR/}"
|
||||||
|
else
|
||||||
|
rel_path="$identity_file"
|
||||||
|
fi
|
||||||
|
identity_paths+=("$rel_path")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Process each IdentityFile with path tracking
|
||||||
|
for i in "${!identity_files[@]}"; do
|
||||||
|
identity_file="${identity_files[$i]}"
|
||||||
|
rel_path="${identity_paths[$i]}"
|
||||||
|
|
||||||
|
# Check if private key exists
|
||||||
|
if [[ -f "$expanded_path" ]]; then
|
||||||
|
# Determine store path
|
||||||
|
local rel_path="${expanded_path#$SSH_DIR/}"
|
||||||
|
rel_path="${rel_path//../_dotdot_}" # Sanitize ..
|
||||||
|
|
||||||
|
local store_path="ssh/$hostname/$rel_path"
|
||||||
|
echo "Importing $expanded_path to $store_path"
|
||||||
|
|
||||||
|
# Insert into pass
|
||||||
|
pass insert --multiline "$store_path" < "$expanded_path" || die "Failed to insert $store_path"
|
||||||
|
else
|
||||||
|
echo "Skipping non-existent IdentityFile: $identity_file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Save Host block
|
||||||
|
local config_store="ssh/$hostname/config"
|
||||||
|
echo "Storing Host block in $config_store"
|
||||||
|
printf "%s\n" "${host_block[@]}" | pass insert --multiline "$config_store" >/dev/null || die "Failed to save config"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export SSH keys and config from pass
|
||||||
|
cmd_export() {
|
||||||
|
local hostname="$1"
|
||||||
|
[[ -z "$hostname" ]] && die "Usage: pass ssh export <hostname>"
|
||||||
|
|
||||||
|
# Retrieve Host block
|
||||||
|
local config_store="ssh/$hostname/config"
|
||||||
|
local host_block
|
||||||
|
host_block=$(pass show "$config_store" 2>/dev/null) || die "No config found for $hostname"
|
||||||
|
|
||||||
|
# Check existing Host entries
|
||||||
|
local existing_patterns=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ ^[Hh][Oo][Ss][Tt][[:space:]]+([^#]+) ]]; then
|
||||||
|
existing_patterns+=("${BASH_REMATCH[1]}")
|
||||||
|
fi
|
||||||
|
done < "$CONFIG_FILE"
|
||||||
|
|
||||||
|
# Check if exported Host patterns exist
|
||||||
|
local exported_patterns
|
||||||
|
if [[ "$host_block" =~ ^[Hh][Oo][Ss][Tt][[:space:]]+([^[:space:]#]+) ]]; then
|
||||||
|
exported_patterns="${BASH_REMATCH[1]}"
|
||||||
|
else
|
||||||
|
die "Invalid Host block in $config_store"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for conflicts
|
||||||
|
local conflict=0
|
||||||
|
for pattern in $exported_patterns; do
|
||||||
|
for existing in "${existing_patterns[@]}"; do
|
||||||
|
if [[ " $existing " == *" $pattern "* ]]; then
|
||||||
|
echo "Conflict: Host pattern '$pattern' exists in $CONFIG_FILE"
|
||||||
|
conflict=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( conflict )) && ! yesno "Overwrite conflicting Host entries?"; then
|
||||||
|
die "Export aborted"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Backup original config
|
||||||
|
local backup="${CONFIG_FILE}.bak.$(date +%s)"
|
||||||
|
cp "$CONFIG_FILE" "$backup" || die "Failed to backup config"
|
||||||
|
|
||||||
|
# Remove conflicting Host blocks
|
||||||
|
awk -v patterns="$exported_patterns" '
|
||||||
|
BEGIN { in_block=0; delete_lines=0 }
|
||||||
|
/^[Hh][Oo][Ss][Tt][[:space:]]+/ {
|
||||||
|
if (in_block) { in_block=0 }
|
||||||
|
split($0, parts, /[[:space:]]+/)
|
||||||
|
for (i=2; i<=NF; i++) {
|
||||||
|
for (p in patterns_arr) {
|
||||||
|
if (parts[i] == patterns_arr[p]) {
|
||||||
|
delete_lines=1
|
||||||
|
in_block=1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
in_block { next }
|
||||||
|
delete_lines { delete_lines=0; next }
|
||||||
|
{ print }
|
||||||
|
' "$backup" > "$CONFIG_FILE" || die "Failed to remove conflicts"
|
||||||
|
|
||||||
|
# Append new Host block
|
||||||
|
echo "Appending Host block for $hostname to $CONFIG_FILE"
|
||||||
|
echo "$host_block" >> "$CONFIG_FILE"
|
||||||
|
|
||||||
|
# Export IdentityFiles
|
||||||
|
local identity_files=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ ^[Ii][Dd][Ee][Nn][Tt][Ii][Tt][Yy][Ff][Ii][Ll][Ee][[:space:]]+([^[:space:]]+) ]]; then
|
||||||
|
identity_files+=("${BASH_REMATCH[1]}")
|
||||||
|
fi
|
||||||
|
done <<< "$host_block"
|
||||||
|
|
||||||
|
for identity_file in "${identity_files[@]}"; do
|
||||||
|
local expanded_path="${identity_file/#\~/$HOME}"
|
||||||
|
expanded_path=$(realpath -m "$expanded_path")
|
||||||
|
|
||||||
|
# Resolve relative to SSH_DIR
|
||||||
|
if [[ "$expanded_path" != "$SSH_DIR"/* ]]; then
|
||||||
|
expanded_path="$SSH_DIR/$identity_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local rel_path="${expanded_path#$SSH_DIR/}"
|
||||||
|
rel_path="${rel_path//../_dotdot_}"
|
||||||
|
local store_path="ssh/$hostname/$rel_path"
|
||||||
|
|
||||||
|
if ! pass show "$store_path" >/dev/null 2>&1; then
|
||||||
|
echo "Warning: $store_path not found in pass"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$expanded_path" ]] && ! yesno "Overwrite $expanded_path?"; then
|
||||||
|
echo "Skipping $expanded_path"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Exporting $store_path to $expanded_path"
|
||||||
|
mkdir -p "$(dirname "$expanded_path")"
|
||||||
|
pass show "$store_path" > "$expanded_path"
|
||||||
|
chmod 600 "$expanded_path"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Export complete. Original config backed up to $backup"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Bulk operations
|
||||||
|
cmd_import_all() {
|
||||||
|
echo "Importing all hosts from $CONFIG_FILE"
|
||||||
|
local hostname=""
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" =~ ^[Hh][Oo][Ss][Tt][[:space:]]+([^#[:space:]]+) ]]; then
|
||||||
|
hostname="${BASH_REMATCH[1]}"
|
||||||
|
echo "Importing host: $hostname"
|
||||||
|
cmd_import "$hostname" || echo "Failed to import $hostname"
|
||||||
|
fi
|
||||||
|
done < "$CONFIG_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_export_all() {
|
||||||
|
echo "Exporting all hosts to $CONFIG_FILE"
|
||||||
|
local hosts=()
|
||||||
|
while IFS= read -r -d '' path; do
|
||||||
|
if [[ "$path" =~ ^ssh/([^/]+)/config ]]; then
|
||||||
|
hosts+=("${BASH_REMATCH[1]}")
|
||||||
|
fi
|
||||||
|
done < <(pass ls ssh | grep -F /config | tr '\n' '\0')
|
||||||
|
|
||||||
|
for host in "${hosts[@]}"; do
|
||||||
|
echo "Exporting host: $host"
|
||||||
|
cmd_export "$host" || echo "Failed to export $host"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main command handler
|
||||||
|
case "$1" in
|
||||||
|
import) shift; cmd_import "$@" ;;
|
||||||
|
export) shift; cmd_export "$@" ;;
|
||||||
|
import-all) shift; cmd_import_all "$@" ;;
|
||||||
|
export-all) shift; cmd_export_all "$@" ;;
|
||||||
|
*) die "Usage: pass ssh import|export|import-all|export-all <hostname>" ;;
|
||||||
|
esac
|
||||||
Loading…
Reference in New Issue