import argparse import json import logging import os import sys from clan_lib.errors import ClanError from clan_lib.git import commit_files from . import sops from .secrets import update_secrets from .sops import ( default_admin_private_key_path, generate_private_key, load_age_plugins, ) log = logging.getLogger(__name__) def generate_key() -> sops.SopsKey: """Generate a new age key and return it as a SopsKey. This function does not check if the key already exists. It will generate a new key every time it is called. Use 'check_key_exists' to check if a key already exists. Before calling this function if you dont want to generate a new key. """ path = default_admin_private_key_path() _, pub_key = generate_private_key(out_file=path) log.info( f"Generated age private key at '{path}' for your user.\nPlease back it up on a secure location or you will lose access to your secrets.", ) return sops.SopsKey( pub_key, username="", key_type=sops.KeyType.AGE, source=str(path), ) def generate_command(args: argparse.Namespace) -> None: pub_keys = sops.maybe_get_admin_public_keys() if not pub_keys or args.new: key = generate_key() pub_keys = [key] for key in pub_keys: key_type = key.key_type.name.lower() print(f"{key.key_type.name} key {key.pubkey} is already set", file=sys.stderr) print( f"Add your {key_type} public key to the repository with:", file=sys.stderr, ) print( f"clan secrets users add --{key_type}-key {key.pubkey}", file=sys.stderr, ) def show_command(_args: argparse.Namespace) -> None: keys = sops.maybe_get_admin_public_keys() if not keys: msg = "No public key found" raise ClanError(msg) json.dump([key.as_dict() for key in keys], sys.stdout, indent=2, sort_keys=True) def update_command(args: argparse.Namespace) -> None: flake_dir = args.flake.path # Only necessary for the `secrets` test in `clan-infra` currently # https://git.clan.lol/clan/clan-infra/src/commit/4cab8e49c3ac0e0395c67abaf789d806807bfb08/checks/secrets.nix # TODO: add a `check` command instead that never loads age plugins # rather than exposing this escape hatch should_load_age_plugins = os.environ.get("CLAN_LOAD_AGE_PLUGINS", "true") != "false" commit_files( update_secrets( flake_dir, age_plugins=load_age_plugins(args.flake) if should_load_age_plugins else None, ), flake_dir, "Updated secrets with new keys", ) def register_key_parser(parser: argparse.ArgumentParser) -> None: subparser = parser.add_subparsers( title="command", description="the command to run", help="the command to run", required=True, ) parser_generate = subparser.add_parser( "generate", description=( "Generate an age key for the Clan, if you already have an age " "or PGP key, then use it to create your user, see: " "`clan secrets users add --help'" ), ) parser_generate.add_argument( "--new", help=( "Generate a new key, without checking if a key already exists. " " This will not overwrite an existing key." ), action="store_true", ) parser_generate.set_defaults(func=generate_command) parser_show = subparser.add_parser("show", help="show public key") parser_show.set_defaults(func=show_command) parser_update = subparser.add_parser( "update", help="re-encrypt all secrets with current keys (useful when changing keys)", ) parser_update.set_defaults(func=update_command)