All checks were successful
buildbot/nix-build .#checks.aarch64-darwin.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-flash-installer Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-iso-installer Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.x86_64-linux.check-for-breakpoints Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-flash-installer Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-iso-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-ts-api Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-cli-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.package-default Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-age Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-archlinux Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-apk Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-e2fsprogs Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-fakeroot Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-openssh Build done.
buildbot/nix-build .#checks.x86_64-linux."clan-dep-python3.11-mypy" Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-webview-ui Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-zbar Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-app-no-breakpoints Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-bash Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-clan-cli Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-test-backup Build done.
buildbot/nix-build .#checks.x86_64-linux.package-deploy-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-inventory-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-default Build done.
buildbot/nix-build .#checks.x86_64-linux.deltachat Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-deb Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-rpm Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-git Build done.
buildbot/nix-build .#checks.x86_64-linux.package-module-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-nix Build done.
buildbot/nix-build .#checks.x86_64-linux.renderClanOptions Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-test-backup Build done.
buildbot/nix-build .#checks.x86_64-linux."clan-dep-python3.11-qemu" Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-cli Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-rsync Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-sops Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-sshpass Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-tor Build done.
buildbot/nix-build .#checks.x86_64-linux.lib-jsonschema-example-valid Build done.
buildbot/nix-build .#checks.x86_64-linux.inventory-schema-checks Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-app-pytest Build done.
buildbot/nix-build .#checks.x86_64-linux.container Build done.
buildbot/nix-build .#checks.x86_64-linux.borgbackup Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-test-backup Build done.
buildbot/nix-build .#checks.x86_64-linux.matrix-synapse Build done.
buildbot/nix-build .#checks.x86_64-linux.package-editor Build done.
buildbot/nix-build .#checks.x86_64-linux.treefmt Build done.
buildbot/nix-build .#checks.x86_64-linux.package-impure-checks Build done.
buildbot/nix-build .#checks.x86_64-linux.package-inventory-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.module-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.package-merge-after-ci Build done.
buildbot/nix-build .#checks.x86_64-linux.package-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.lib-jsonschema-nix-unit-tests Build done.
buildbot/nix-build .#checks.x86_64-linux.postgresql Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-iso-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.package-webview-ui Build done.
checks / checks-impure (pull_request) Successful in 2m13s
buildbot/nix-eval Build done.
buildbot/nix-build .#checks.x86_64-linux.package-tea-create-pr Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zerotier-members Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zerotierone Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zt-tcp-relay Build done.
buildbot/nix-build .#checks.x86_64-linux.package-moonlight-sunshine-accept Build done.
buildbot/nix-build .#checks.x86_64-linux.package-pending-reviews Build done.
buildbot/nix-build .#checks.x86_64-linux.package-function-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.package-module-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-flash-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.secrets Build done.
buildbot/nix-build .#checks.x86_64-linux.package-iso-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.template-minimal Build done.
buildbot/nix-build .#checks.x86_64-linux.zt-tcp-relay Build done.
buildbot/nix-build .#checks.x86_64-linux.wayland-proxy-virtwl Build done.
buildbot/nix-build .#checks.x86_64-linux.syncthing Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-clan-app Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-pytest-without-core Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-app Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-install-test-ubuntu-22-04 Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-pytest-with-core Build done.
buildbot/nix-build .#checks.x86_64-linux.test-backups Build done.
buildbot/nix-build .#checks.x86_64-linux.flash Build done.
buildbot/nix-build .#checks.x86_64-linux.test-installation Build done.
235 lines
8.1 KiB
Python
235 lines
8.1 KiB
Python
import io
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from collections.abc import Iterator
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
from tempfile import NamedTemporaryFile
|
|
from typing import IO
|
|
|
|
from ..cmd import Log, run
|
|
from ..dirs import user_config_dir
|
|
from ..errors import ClanError
|
|
from ..nix import nix_shell
|
|
from .folders import sops_machines_folder, sops_users_folder
|
|
|
|
|
|
class SopsKey:
|
|
def __init__(self, pubkey: str, username: str) -> None:
|
|
self.pubkey = pubkey
|
|
self.username = username
|
|
|
|
|
|
def get_public_key(privkey: str) -> str:
|
|
cmd = nix_shell(["nixpkgs#age"], ["age-keygen", "-y"])
|
|
try:
|
|
res = subprocess.run(
|
|
cmd, input=privkey, stdout=subprocess.PIPE, text=True, check=True
|
|
)
|
|
except subprocess.CalledProcessError as e:
|
|
raise ClanError(
|
|
"Failed to get public key for age private key. Is the key malformed?"
|
|
) from e
|
|
return res.stdout.strip()
|
|
|
|
|
|
def generate_private_key(out_file: Path | None = None) -> tuple[str, str]:
|
|
cmd = nix_shell(["nixpkgs#age"], ["age-keygen"])
|
|
try:
|
|
proc = run(cmd)
|
|
res = proc.stdout.strip()
|
|
pubkey = None
|
|
private_key = None
|
|
for line in res.splitlines():
|
|
if line.startswith("# public key:"):
|
|
pubkey = line.split(":")[1].strip()
|
|
if not line.startswith("#"):
|
|
private_key = line
|
|
if not pubkey:
|
|
raise ClanError("Could not find public key in age-keygen output")
|
|
if not private_key:
|
|
raise ClanError("Could not find private key in age-keygen output")
|
|
if out_file:
|
|
out_file.parent.mkdir(parents=True, exist_ok=True)
|
|
out_file.write_text(res)
|
|
return private_key, pubkey
|
|
except subprocess.CalledProcessError as e:
|
|
raise ClanError("Failed to generate private sops key") from e
|
|
|
|
|
|
def get_user_name(flake_dir: Path, user: str) -> str:
|
|
"""Ask the user for their name until a unique one is provided."""
|
|
while True:
|
|
name = input(
|
|
f"Your key is not yet added to the repository. Enter your user name for which your sops key will be stored in the repository [default: {user}]: "
|
|
)
|
|
if name:
|
|
user = name
|
|
if not (flake_dir / user).exists():
|
|
return user
|
|
print(f"{flake_dir / user} already exists")
|
|
|
|
|
|
def ensure_user_or_machine(flake_dir: Path, pub_key: str) -> SopsKey:
|
|
key = SopsKey(pub_key, username="")
|
|
folders = [sops_users_folder(flake_dir), sops_machines_folder(flake_dir)]
|
|
|
|
for folder in folders:
|
|
if folder.exists():
|
|
for user in folder.iterdir():
|
|
if not (user / "key.json").exists():
|
|
continue
|
|
if read_key(user) == pub_key:
|
|
key.username = user.name
|
|
return key
|
|
|
|
raise ClanError(
|
|
f"Your sops key is not yet added to the repository. Please add it with 'clan secrets users add youruser {pub_key}' (replace youruser with your user name)"
|
|
)
|
|
|
|
|
|
def default_sops_key_path() -> Path:
|
|
raw_path = os.environ.get("SOPS_AGE_KEY_FILE")
|
|
if raw_path:
|
|
return Path(raw_path)
|
|
else:
|
|
return user_config_dir() / "sops" / "age" / "keys.txt"
|
|
|
|
|
|
def ensure_sops_key(flake_dir: Path) -> SopsKey:
|
|
key = os.environ.get("SOPS_AGE_KEY")
|
|
if key:
|
|
return ensure_user_or_machine(flake_dir, get_public_key(key))
|
|
path = default_sops_key_path()
|
|
if path.exists():
|
|
return ensure_user_or_machine(flake_dir, get_public_key(path.read_text()))
|
|
else:
|
|
raise ClanError(
|
|
"No sops key found. Please generate one with 'clan secrets key generate'."
|
|
)
|
|
|
|
|
|
@contextmanager
|
|
def sops_manifest(keys: list[str]) -> Iterator[Path]:
|
|
with NamedTemporaryFile(delete=False, mode="w") as manifest:
|
|
json.dump(
|
|
dict(creation_rules=[dict(key_groups=[dict(age=keys)])]), manifest, indent=2
|
|
)
|
|
manifest.flush()
|
|
yield Path(manifest.name)
|
|
|
|
|
|
def update_keys(secret_path: Path, keys: list[str]) -> list[Path]:
|
|
with sops_manifest(keys) as manifest:
|
|
secret_path = secret_path / "secret"
|
|
time_before = secret_path.stat().st_mtime
|
|
cmd = nix_shell(
|
|
["nixpkgs#sops"],
|
|
[
|
|
"sops",
|
|
"--config",
|
|
str(manifest),
|
|
"updatekeys",
|
|
"--yes",
|
|
str(secret_path),
|
|
],
|
|
)
|
|
run(cmd, log=Log.BOTH, error_msg=f"Could not update keys for {secret_path}")
|
|
if time_before == secret_path.stat().st_mtime:
|
|
return []
|
|
return [secret_path]
|
|
|
|
|
|
def encrypt_file(
|
|
secret_path: Path, content: IO[str] | str | bytes | None, keys: list[str]
|
|
) -> None:
|
|
folder = secret_path.parent
|
|
folder.mkdir(parents=True, exist_ok=True)
|
|
|
|
with sops_manifest(keys) as manifest:
|
|
if not content:
|
|
args = ["sops", "--config", str(manifest)]
|
|
args.extend([str(secret_path)])
|
|
cmd = nix_shell(["nixpkgs#sops"], args)
|
|
# Don't use our `run` here, because it breaks editor integration.
|
|
# We never need this in our UI.
|
|
p = subprocess.run(cmd, check=False)
|
|
# returns 200 if the file is changed
|
|
if p.returncode != 0 and p.returncode != 200:
|
|
raise ClanError(
|
|
f"Failed to encrypt {secret_path}: sops exited with {p.returncode}"
|
|
)
|
|
return
|
|
|
|
# hopefully /tmp is written to an in-memory file to avoid leaking secrets
|
|
with NamedTemporaryFile(delete=False) as f:
|
|
try:
|
|
if isinstance(content, str):
|
|
with open(f.name, "w") as fd:
|
|
fd.write(content)
|
|
elif isinstance(content, bytes):
|
|
with open(f.name, "wb") as fd:
|
|
fd.write(content)
|
|
elif isinstance(content, io.IOBase):
|
|
with open(f.name, "w") as fd:
|
|
shutil.copyfileobj(content, fd)
|
|
else:
|
|
raise ClanError(f"Invalid content type: {type(content)}")
|
|
# we pass an empty manifest to pick up existing configuration of the user
|
|
args = ["sops", "--config", str(manifest)]
|
|
args.extend(["-i", "--encrypt", str(f.name)])
|
|
cmd = nix_shell(["nixpkgs#sops"], args)
|
|
run(cmd, log=Log.BOTH)
|
|
# atomic copy of the encrypted file
|
|
with NamedTemporaryFile(dir=folder, delete=False) as f2:
|
|
shutil.copyfile(f.name, f2.name)
|
|
os.rename(f2.name, secret_path)
|
|
finally:
|
|
try:
|
|
os.remove(f.name)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def decrypt_file(secret_path: Path) -> str:
|
|
with sops_manifest([]) as manifest:
|
|
cmd = nix_shell(
|
|
["nixpkgs#sops"],
|
|
["sops", "--config", str(manifest), "--decrypt", str(secret_path)],
|
|
)
|
|
res = run(cmd, error_msg=f"Could not decrypt {secret_path}")
|
|
return res.stdout
|
|
|
|
|
|
def write_key(path: Path, publickey: str, overwrite: bool) -> None:
|
|
path.mkdir(parents=True, exist_ok=True)
|
|
try:
|
|
flags = os.O_CREAT | os.O_WRONLY | os.O_TRUNC
|
|
if not overwrite:
|
|
flags |= os.O_EXCL
|
|
fd = os.open(path / "key.json", flags)
|
|
except FileExistsError:
|
|
raise ClanError(
|
|
f"{path.name} already exists in {path}. Use --force to overwrite."
|
|
)
|
|
with os.fdopen(fd, "w") as f:
|
|
json.dump({"publickey": publickey, "type": "age"}, f, indent=2)
|
|
|
|
|
|
def read_key(path: Path) -> str:
|
|
with open(path / "key.json") as f:
|
|
try:
|
|
key = json.load(f)
|
|
except json.JSONDecodeError as e:
|
|
raise ClanError(f"Failed to decode {path.name}: {e}")
|
|
if key["type"] != "age":
|
|
raise ClanError(
|
|
f"{path.name} is not an age key but {key['type']}. This is not supported"
|
|
)
|
|
publickey = key.get("publickey")
|
|
if not publickey:
|
|
raise ClanError(f"{path.name} does not contain a public key")
|
|
return publickey
|