diff --git a/nixos/doc/manual/release-notes/rl-2311.section.md b/nixos/doc/manual/release-notes/rl-2311.section.md index c3cb495498df..822ba67a40df 100644 --- a/nixos/doc/manual/release-notes/rl-2311.section.md +++ b/nixos/doc/manual/release-notes/rl-2311.section.md @@ -125,6 +125,8 @@ - [Rosenpass](https://rosenpass.eu/), a service for post-quantum-secure VPNs with WireGuard. Available as [services.rosenpass](#opt-services.rosenpass.enable). +- [c2FmZQ](https://github.com/c2FmZQ/c2FmZQ/), an application that can securely encrypt, store, and share files, including but not limited to pictures and videos. Available as [services.c2fmzq-server](#opt-services.c2fmzq-server.enable). + ## Backward Incompatibilities {#sec-release-23.11-incompatibilities} - `network-online.target` has been fixed to no longer time out for systems with `networking.useDHCP = true` and `networking.useNetworkd = true`. diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index bf24ee7603fd..4d8fa8159a89 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1232,6 +1232,7 @@ ./services/web-apps/atlassian/jira.nix ./services/web-apps/audiobookshelf.nix ./services/web-apps/bookstack.nix + ./services/web-apps/c2fmzq-server.nix ./services/web-apps/calibre-web.nix ./services/web-apps/coder.nix ./services/web-apps/changedetection-io.nix diff --git a/nixos/modules/services/web-apps/c2fmzq-server.md b/nixos/modules/services/web-apps/c2fmzq-server.md new file mode 100644 index 000000000000..236953bd4ff7 --- /dev/null +++ b/nixos/modules/services/web-apps/c2fmzq-server.md @@ -0,0 +1,42 @@ +# c2FmZQ {#module-services-c2fmzq} + +c2FmZQ is an application that can securely encrypt, store, and share files, +including but not limited to pictures and videos. + +The service `c2fmzq-server` can be enabled by setting +``` +{ + services.c2fmzq-server.enable = true; +} +``` +This will spin up an instance of the server which is API-compatible with +[Stingle Photos](https://stingle.org) and an experimental Progressive Web App +(PWA) to interact with the storage via the browser. + +In principle the server can be exposed directly on a public interface and there +are command line options to manage HTTPS certificates directly, but the module +is designed to be served behind a reverse proxy or only accessed via localhost. + +``` +{ + services.c2fmzq-server = { + enable = true; + bindIP = "127.0.0.1"; # default + port = 8080; # default + }; + + services.nginx = { + enable = true; + recommendedProxySettings = true; + virtualHosts."example.com" = { + enableACME = true; + forceSSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:8080"; + }; + }; + }; +} +``` + +For more information, see . diff --git a/nixos/modules/services/web-apps/c2fmzq-server.nix b/nixos/modules/services/web-apps/c2fmzq-server.nix new file mode 100644 index 000000000000..2749c2a5a87a --- /dev/null +++ b/nixos/modules/services/web-apps/c2fmzq-server.nix @@ -0,0 +1,125 @@ +{ lib, pkgs, config, ... }: + +let + inherit (lib) mkEnableOption mkPackageOption mkOption types; + + cfg = config.services.c2fmzq-server; + + argsFormat = { + type = with lib.types; nullOr (oneOf [ bool int str ]); + generate = lib.cli.toGNUCommandLineShell { }; + }; +in { + options.services.c2fmzq-server = { + enable = mkEnableOption "c2fmzq-server"; + + bindIP = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "The local address to use."; + }; + + port = mkOption { + type = types.port; + default = 8080; + description = "The local port to use."; + }; + + passphraseFile = mkOption { + type = types.str; + example = "/run/secrets/c2fmzq/pwfile"; + description = "Path to file containing the database passphrase"; + }; + + package = mkPackageOption pkgs "c2fmzq" { }; + + settings = mkOption { + type = types.submodule { + freeformType = argsFormat.type; + + options = { + address = mkOption { + internal = true; + type = types.str; + default = "${cfg.bindIP}:${toString cfg.port}"; + }; + + database = mkOption { + type = types.str; + default = "%S/c2fmzq-server/data"; + description = "Path of the database"; + }; + + verbose = mkOption { + type = types.ints.between 1 3; + default = 2; + description = "The level of logging verbosity: 1:Error 2:Info 3:Debug"; + }; + }; + }; + description = '' + Configuration for c2FmZQ-server passed as CLI arguments. + Run {command}`c2FmZQ-server help` for supported values. + ''; + example = { + verbose = 3; + allow-new-accounts = true; + auto-approve-new-accounts = true; + encrypt-metadata = true; + enable-webapp = true; + }; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.c2fmzq-server = { + description = "c2FmZQ-server"; + documentation = [ "https://github.com/c2FmZQ/c2FmZQ/blob/main/README.md" ]; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" "network-online.target" ]; + + serviceConfig = { + ExecStart = "${lib.getExe cfg.package} ${argsFormat.generate cfg.settings}"; + AmbientCapabilities = ""; + CapabilityBoundingSet = ""; + DynamicUser = true; + Environment = "C2FMZQ_PASSPHRASE_FILE=%d/passphrase-file"; + IPAccounting = true; + IPAddressAllow = cfg.bindIP; + IPAddressDeny = "any"; + LoadCredential = "passphrase-file:${cfg.passphraseFile}"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateIPC = true; + PrivateTmp = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SocketBindAllow = cfg.port; + SocketBindDeny = "any"; + StateDirectory = "c2fmzq-server"; + SystemCallArchitectures = "native"; + SystemCallFilter = [ "@system-service" "~@privileged @obsolete" ]; + }; + }; + }; + + meta = { + doc = ./c2fmzq-server.md; + maintainers = with lib.maintainers; [ hmenke ]; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 3531930d863a..2bff8d6cfba6 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -153,6 +153,7 @@ in { budgie = handleTest ./budgie.nix {}; buildbot = handleTest ./buildbot.nix {}; buildkite-agents = handleTest ./buildkite-agents.nix {}; + c2fmzq = handleTest ./c2fmzq.nix {}; caddy = handleTest ./caddy.nix {}; cadvisor = handleTestOn ["x86_64-linux"] ./cadvisor.nix {}; cage = handleTest ./cage.nix {}; diff --git a/nixos/tests/c2fmzq.nix b/nixos/tests/c2fmzq.nix new file mode 100644 index 000000000000..d8ec816c7d29 --- /dev/null +++ b/nixos/tests/c2fmzq.nix @@ -0,0 +1,75 @@ +import ./make-test-python.nix ({ pkgs, lib, ... }: { + name = "c2FmZQ"; + meta.maintainers = with lib.maintainers; [ hmenke ]; + + nodes.machine = { + services.c2fmzq-server = { + enable = true; + port = 8080; + passphraseFile = builtins.toFile "pwfile" "hunter2"; # don't do this on real deployments + settings = { + verbose = 3; # debug + }; + }; + environment = { + sessionVariables = { + C2FMZQ_PASSPHRASE = "lol"; + C2FMZQ_API_SERVER = "http://localhost:8080"; + }; + systemPackages = [ + pkgs.c2fmzq + (pkgs.writeScriptBin "c2FmZQ-client-wrapper" '' + #!${pkgs.expect}/bin/expect -f + spawn c2FmZQ-client {*}$argv + expect { + "Enter password:" { send "$env(PASSWORD)\r" } + "Type YES to confirm:" { send "YES\r" } + timeout { exit 1 } + eof { exit 0 } + } + interact + '') + ]; + }; + }; + + testScript = { nodes, ... }: '' + machine.start() + machine.wait_for_unit("c2fmzq-server.service") + machine.wait_for_open_port(8080) + + with subtest("Create accounts for alice and bob"): + machine.succeed("PASSWORD=foobar c2FmZQ-client-wrapper -- -v 3 create-account alice@example.com") + machine.succeed("PASSWORD=fizzbuzz c2FmZQ-client-wrapper -- -v 3 create-account bob@example.com") + + with subtest("Log in as alice"): + machine.succeed("PASSWORD=foobar c2FmZQ-client-wrapper -- -v 3 login alice@example.com") + msg = machine.succeed("c2FmZQ-client -v 3 status") + assert "Logged in as alice@example.com" in msg, f"ERROR: Not logged in as alice:\n{msg}" + + with subtest("Create a new album, upload a file, and delete the uploaded file"): + machine.succeed("c2FmZQ-client -v 3 create-album 'Rarest Memes'") + machine.succeed("echo 'pls do not steal' > meme.txt") + machine.succeed("c2FmZQ-client -v 3 import meme.txt 'Rarest Memes'") + machine.succeed("c2FmZQ-client -v 3 sync") + machine.succeed("rm meme.txt") + + with subtest("Share the album with bob"): + machine.succeed("c2FmZQ-client-wrapper -- -v 3 share 'Rarest Memes' bob@example.com") + + with subtest("Log in as bob"): + machine.succeed("PASSWORD=fizzbuzz c2FmZQ-client-wrapper -- -v 3 login bob@example.com") + msg = machine.succeed("c2FmZQ-client -v 3 status") + assert "Logged in as bob@example.com" in msg, f"ERROR: Not logged in as bob:\n{msg}" + + with subtest("Download the shared file"): + machine.succeed("c2FmZQ-client -v 3 download 'shared/Rarest Memes/meme.txt'") + machine.succeed("c2FmZQ-client -v 3 export 'shared/Rarest Memes/meme.txt' .") + msg = machine.succeed("cat meme.txt") + assert "pls do not steal\n" == msg, f"File content is not the same:\n{msg}" + + with subtest("Test that PWA is served"): + msg = machine.succeed("curl -sSfL http://localhost:8080") + assert "c2FmZQ" in msg, f"Could not find 'c2FmZQ' in the output:\n{msg}" + ''; +}) diff --git a/pkgs/by-name/c2/c2fmzq/package.nix b/pkgs/by-name/c2/c2fmzq/package.nix new file mode 100644 index 000000000000..b7098b9a7583 --- /dev/null +++ b/pkgs/by-name/c2/c2fmzq/package.nix @@ -0,0 +1,36 @@ +{ lib +, buildGoModule +, fetchFromGitHub +, nixosTests +}: + +buildGoModule rec { + pname = "c2FmZQ"; + version = "0.4.8"; + + src = fetchFromGitHub { + owner = "c2FmZQ"; + repo = "c2FmZQ"; + rev = "v${version}"; + hash = "sha256-IYSmGzjTDMBgEMVZsi6CuUz6L7BzpmbrJYVPUhFr7rw="; + }; + + ldflags = [ "-s" "-w" ]; + + sourceRoot = "source/c2FmZQ"; + + vendorHash = "sha256-Hz6P+ptn1i+8Ek3pp8j+iB8NN5Xks50jyZuT8Ullxbo="; + + subPackages = [ "c2FmZQ-client" "c2FmZQ-server" ]; + + passthru.tests = { inherit (nixosTests) c2fmzq; }; + + meta = with lib; { + description = "Securely encrypt, store, and share files, including but not limited to pictures and videos"; + homepage = "https://github.com/c2FmZQ/c2FmZQ"; + license = licenses.gpl3Only; + mainProgram = "c2FmZQ-server"; + maintainers = with maintainers; [ hmenke ]; + platforms = platforms.linux; + }; +}