This commit is contained in:
fenris 2026-04-29 23:46:44 +02:00
commit a056443224
8 changed files with 608 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/.geany

21
misc/put Executable file
View file

@ -0,0 +1,21 @@
#!/usr/bin/env sh
## consts
path_conf_source="build/config"
path_keys_source="build/keypairs"
## vars
datestring=$(date +%Y-%m-%d)
path_conf_target="${HOME}/.ssh/config"
path_conf_backup="${path_conf_target}.${datestring}.bak"
path_keys_target="${HOME}/.ssh/keypairs"
path_keys_backup="${HOME}/.ssh/keypairs.${datestring}.bak"
## exec
test -e ${path_conf_target} || cp -v ${path_conf_target} ${path_conf_backup}
mkdir -p $(dirname ${path_conf_target})
cp -v ${path_conf_source} ${path_conf_target}
test -e ${path_keys_target} || (mkdir -p ${path_keys_backup} && cp -r -u -v ${path_keys_target}/* ${path_keys_backup}/)
mkdir -p ${path_keys_target}
cp -r -u -v ${path_keys_source}/* ${path_keys_target}/

25
readme.md Normal file
View file

@ -0,0 +1,25 @@
# bifroyst
## Erstellung
### Voraussetzungen
(keine)
### Anweisungen
- `tools/build` ausführen
### Betrieb
### Voraussetzungen
- Python3
- KeepassXC
### Anweisungen
- siehe `bifroyst -h`

139
source/helpers/keepass.py Normal file
View file

@ -0,0 +1,139 @@
import helpers.misc as __helpers_misc
def compose_entry(
group_name,
member_name
):
return __helpers_misc.string_coin(
"{{group}}/{{member}}",
{
"group": group_name,
"member": member_name,
}
)
def macro_auth(
key_file
):
return (
""
if
(key_file is None)
else
__helpers_misc.string_coin(
" --no-password --key-file={{authfile_path}}",
{
"authfile_path": key_file,
}
)
)
def action_db_create(
db_path,
options = None
):
options = (
{
"key_file": None,
}
|
(options or {})
)
__helpers_misc.shell_exec(
__helpers_misc.string_coin(
"keepassxc-cli db-create{{macro_auth}} {{db_path}}",
{
"db_path": db_path,
"macro_auth": (
""
if
(options["key_file"] is None)
else
__helpers_misc.string_coin(
" --set-key-file={{authfile_path}}",
{
"authfile_path": options["key_file"],
}
)
)
}
)
)
def action_mkdir(
db_path,
group_name,
options = None
):
__helpers_misc.shell_exec(
__helpers_misc.string_coin(
"keepassxc-cli mkdir{{macro_auth}} {{db_path}} {{group_name}}",
{
"db_path": db_path,
"group_name": group_name,
"macro_auth": macro_auth(options["key_file"]),
}
)
)
def action_add(
db_path,
group_name,
member_name,
options = None
):
__helpers_misc.shell_exec(
__helpers_misc.string_coin(
"keepassxc-cli add{{macro_auth}} {{db_path}} {{entry}}",
{
"db_path": db_path,
"entry": compose_entry(group_name, member_name),
"macro_auth": macro_auth(options["key_file"]),
}
)
)
def action_rm(
db_path,
group_name,
member_name,
options = None
):
__helpers_misc.shell_exec(
__helpers_misc.string_coin(
"keepassxc-cli rm{{macro_auth}} {{db_path}} {{entry}}",
{
"db_path": db_path,
"entry": compose_entry(group_name, member_name),
"macro_auth": macro_auth(options["key_file"]),
}
)
)
def action_attachment_import(
db_path,
group_name,
member_name,
attachment_label,
attachment_path,
options = None
):
__helpers_misc.shell_exec(
__helpers_misc.string_coin(
"keepassxc-cli attachment-import{{macro_auth}} {{db_path}} {{entry}} '{{attachment_label}}' {{attachment_path}}",
{
"db_path": db_path,
"entry": compose_entry(group_name, member_name),
"attachment_label": attachment_label,
"attachment_path": attachment_path,
"macro_auth": macro_auth(options["key_file"]),
}
)
)

50
source/helpers/misc.py Normal file
View file

@ -0,0 +1,50 @@
import sys as _sys
import os as _os
def convey(value, functions):
result = value
for function in functions:
result = function(result)
return result
def string_coin(template, arguments):
result = template
for (key, value, ) in arguments.items():
result = result.replace("{{%s}}" % key, value)
return result
def file_read(path):
handle = open(path, "r")
content = handle.read()
handle.close()
return content
def file_write(path, content):
handle = open(path, "w")
handle.write(content)
handle.close()
def directory_create(path):
steps = _os.path.split(path)
for index in range(len(steps)):
path_ = _os.path.join(*(steps[:(index+1)]))
if (path_ == ""):
pass
else:
if (_os.path.exists(path_)):
pass
else:
_os.mkdir(path_)
def shell_exec(
command
):
_sys.stderr.write("\n>> %s\n" % command)
_os.system(command)

107
source/helpers/ssh.py Normal file
View file

@ -0,0 +1,107 @@
import helpers.misc as __misc
def sshconf_decode_bool(
value
):
return (value == "yes")
def sshconf_decode(
sshconf
):
state = {
"name": "initial",
"entries": {},
"current_name": None,
"current_entry": None,
}
for line in sshconf.split("\n"):
line_stripped = line.strip()
if (line_stripped == ""):
pass
else:
parts = line_stripped.split(" ")
head = parts[0]
body = " ".join(parts[1:])
if (head == "#"):
state["current_entry"]["extra"] = (
[body]
if ("extra" not in state["current_entry"]) else
(state["current_entry"]["extra"] + [body])
)
elif (head.lower() == "host"):
if ((state["current_name"] is None) or (state["current_entry"] is None)):
pass
else:
state["entries"][state["current_name"]] = state["current_entry"]
state["current_name"] = body
state["current_entry"] = {}
elif (head.lower() == "hostname"):
state["current_entry"]["host"] = body
elif (head.lower() == "port"):
state["current_entry"]["port"] = int(body)
elif (head.lower() == "user"):
state["current_entry"]["username"] = body
elif (head.lower() == "identityfile"):
state["current_entry"]["key_name"] = body
elif (head.lower() == "proxyjump"):
state["current_entry"]["proxy_jump"] = body
elif (head.lower() == "proxycommand"):
state["current_entry"]["proxy_command"] = body
elif (head.lower() == "pubkeyauthentication"):
state["current_entry"]["pub_key_authentication"] = sshconf_decode_bool(body)
elif (head.lower() == "identitiesonly"):
state["current_entry"]["identities_only"] = sshconf_decode_bool(body)
else:
raise ValueError("unhandled sshconf thing: " + head)
state["entries"][state["current_name"]] = state["current_entry"]
state["current_name"] = None
state["current_entry"] = None
return state["entries"]
def sshconf_encode_bool(
value
):
return ("yes" if value else "no")
def sshconf_encode_entry(name, entry, prefix):
lines = []
lines.append(__misc.string_coin("Host {{prefix}}{{name}}", {"prefix": prefix, "name": name}))
if ("host" in entry):
lines.append(__misc.string_coin("\tHostName {{host}}", {"host": entry["host"]}))
if ("port" in entry):
lines.append(__misc.string_coin("\tPort {{port}}", {"port": ("%u" % entry["port"])}))
if ("username" in entry):
lines.append(__misc.string_coin("\tUser {{username}}", {"username": entry["username"]}))
if ("key_name" in entry):
lines.append(__misc.string_coin("\tIdentityFile {{key_path}}", {"key_path": ("~/.ssh/keypairs/%s%s" % (prefix, entry["key_name"]))}))
if ("proxy_jump" in entry):
lines.append(__misc.string_coin("\tProxyJump {{proxy_jump}}", {"proxy_jump": entry["proxy_jump"]}))
if ("proxy_command" in entry):
lines.append(__misc.string_coin("\tProxyCommand {{proxy_command}}", {"proxy_command": entry["proxy_command"]}))
if ("pub_key_authentication" in entry):
lines.append(__misc.string_coin("\tPubKeyAuthentication {{pub_key_authentication}}", {"pub_key_authentication": sshconf_encode_bool(entry["pub_key_authentication"])}))
if ("identities_only" in entry):
lines.append(__misc.string_coin("\tIdentitiesOnly {{identities_only}}", {"identities_only": sshconf_encode_bool(entry["identities_only"])}))
lines.append("")
return "\n".join(lines)
def sshconf_encode(conf):
return "\n".join(
map(
lambda pair: sshconf_encode_entry(
pair[0],
pair[1],
conf["settings"]["prefix"]
),
sorted(
conf["entries"].items(),
key = lambda entry: entry[0]
)
)
)

233
source/main.py Executable file
View file

@ -0,0 +1,233 @@
import sys as _sys
import os as _os
import json as _json
import shutil as _shutil
import argparse as _argparse
import helpers.misc as __helpers_misc
import helpers.ssh as __helpers_ssh
import helpers.keepass as __helpers_keepass
def action_init(
source_directory
):
keepass_db_path = _os.path.join(source_directory, "private_keys.kdbx")
keepass_authfile_path = _os.path.join(source_directory, "private_keys.keyx")
__helpers_misc.directory_create(source_directory)
__helpers_misc.shell_exec(
__helpers_misc.string_coin(
"openssl rand -out {{authfile_path}} 256",
{
"authfile_path": keepass_authfile_path,
}
)
)
__helpers_keepass.action_db_create(
keepass_db_path,
{
"key_file": keepass_authfile_path,
}
)
def action_key_add(
source_directory,
group,
name
):
directory_key = _os.path.join(source_directory, group, "keys")
path_key_private = _os.path.join(directory_key, name)
path_key_public = ("%s.pub" % path_key_private)
__helpers_misc.directory_create(directory_key)
## generate
__helpers_misc.shell_exec(
__helpers_misc.string_coin(
"ssh-keygen -t {{encryption_type}} -f {{path}}",
{
"encryption_type": "ed25519",
"path": path_key_private,
}
)
)
## transfer private key to keepass database
if True:
keepass_db_path = _os.path.join(source_directory, "private_keys.kdbx")
keepass_authfile_path = _os.path.join(source_directory, "private_keys.keyx")
__helpers_keepass.action_mkdir(
keepass_db_path,
group,
{
"key_file": keepass_authfile_path,
}
)
__helpers_keepass.action_add(
keepass_db_path,
group,
name,
{
"key_file": keepass_authfile_path,
}
)
__helpers_keepass.action_attachment_import(
keepass_db_path,
group,
name,
'ssh private key',
path_key_private,
{
"key_file": keepass_authfile_path,
}
)
## remove private key file
__helpers_misc.shell_exec(
__helpers_misc.string_coin(
"rm --force {{path}}",
{
"path": path_key_private,
}
)
)
def action_key_remove(
source_directory,
group,
name
):
path_key_private = _os.path.join(source_directory, group, "keys", name)
path_key_public = ("%s.pub" % path_key_private)
## remove private key from keepass database
if True:
keepass_db_path = _os.path.join(source_directory, "private_keys.kdbx")
keepass_authfile_path = _os.path.join(source_directory, "private_keys.keyx")
__helpers_keepass.action_rm(
keepass_db_path,
group,
name,
{
"key_file": keepass_authfile_path,
}
)
## remove public key file
__helpers_misc.shell_exec(
__helpers_misc.string_coin(
"rm --force {{path}}",
{
"path": path_key_public,
}
)
)
'''
todo: backup (see old put script)
'''
def action_put(
source_directory,
target_directory
):
sshconf = ""
for group in _os.listdir(source_directory):
conf_path = _os.path.join(source_directory, group, "conf.json")
conf_content = __helpers_misc.file_read(conf_path)
conf_data = _json.loads(conf_content)
# conf
if True:
sshconf = (sshconf + "\n" + __helpers_ssh.sshconf_encode(conf_data))
# public keys
if True:
__helpers_misc.directory_create(target_directory)
for name in _os.listdir(_os.path.join(source_directory, group, "keys")):
_shutil.copy(
_os.path.join(source_directory, group, "keys", name),
_os.path.join(target_directory, "%s%s" % (conf_data["settings"]["prefix"], name, ))
)
# private keys
if True:
keepass_db_path = _os.path.join(source_directory, "private_keys.kdbx")
_shutil.copy(
keepass_db_path,
_os.path.join(target_directory, "private_keys.kdbx")
)
## todo: keyfile
__helpers_misc.file_write(_os.path.join(target_directory, "config"), sshconf)
def main():
## consts
dir_conf = "source/conf"
path_build = "build/config"
## args
argument_parser = _argparse.ArgumentParser(
prog = "bifroyst",
description = "SSH connection manager",
)
argument_parser.add_argument(
"action",
type = str,
choices = [
"init",
"key-add",
"key-remove",
"put",
],
default = "put",
metavar = "<action>",
help = "options: init | key-add | key-remove | put",
)
argument_parser.add_argument(
"-s",
"--source-directory",
type = str,
default = ".",
metavar = "<source-directory>",
)
argument_parser.add_argument(
"-t",
"--target-directory",
type = str,
default = "~/.ssh",
metavar = "<target-directory>",
)
argument_parser.add_argument(
"-g",
"--group",
type = str,
default = "default",
metavar = "<group>",
)
argument_parser.add_argument(
"-n",
"--name",
type = str,
default = None,
metavar = "<name>",
)
args = argument_parser.parse_args()
## exec
if (args.action == "init"):
action_init(
args.source_directory
)
elif (args.action == "key-add"):
action_key_add(
args.source_directory,
args.group,
args.name
)
elif (args.action == "key-remove"):
action_key_remove(
args.source_directory,
args.group,
args.name
)
elif (args.action == "put"):
action_put(
args.source_directory,
args.target_directory
)
else:
raise ValueError("invalid action: %s", args.action)

32
tools/build Executable file
View file

@ -0,0 +1,32 @@
#!/usr/bin/env sh
## consts
dir_source=source
dir_temp=/tmp/bifroyst-temp
dir_build=/tmp/bifroyst
## exec
### exec
path_app=${dir_build}/bifroyst
rm ${dir_temp} --force --recursive
mkdir ${dir_temp} --parents
cp ${dir_source}/. ${dir_temp}/ --recursive --update
for dir in $(find ${dir_temp} -mindepth 1 -type d) ; do touch ${dir}/__init__.py ; done
echo '' > ${dir_temp}/__main__.py
echo 'from main import *' >> ${dir_temp}/__main__.py
echo 'if __name__ == "__main__": main()' >> ${dir_temp}/__main__.py
mkdir ${dir_build} --parents
# rm ${path_app}.zip --force
cd ${dir_temp} && python3 -m zipfile -c ${path_app}.zip . ; cd - > /dev/null
echo '#!/usr/bin/env python3' > ${path_app}
cat ${path_app}.zip >> ${path_app}
rm ${path_app}.zip
chmod +x ${path_app}
echo "-- ${dir_build}"