From 30f031fe1c2877d8efb9584b1c0c80766f05d763 Mon Sep 17 00:00:00 2001
From: Dustin Stiles <duwstiles@pm.me>
Date: Mon, 10 Mar 2025 19:53:10 -0400
Subject: [PATCH] sacrifice code quality (temp) for ux

---
 README.md            |  48 +++++++++++++----
 cmd/dsfxctl/main.go  |  60 +++++++++++++++++----
 cmd/dsfxnode/main.go | 126 +++++++++++++++++++++++++++++++++++++------
 config.json          |   6 ---
 4 files changed, 199 insertions(+), 41 deletions(-)
 delete mode 100644 config.json

diff --git a/README.md b/README.md
index 129195c..56f3409 100644
--- a/README.md
+++ b/README.md
@@ -46,15 +46,33 @@ go install ./cmd/...
 
 ## Usage
 
+**WARNING:** The dsfx project is still in development and should not be used in a
+production environment. The following instructions are for testing and development
+purposes only. The system implements it's own cryptography and has not been audited
+by a third party.
+
+Currently, the target audience consists of developers, testers, and security
+researchers who are interested in secure file exchange systems. We also welcome
+homelab enthusiasts, but do **not recommened** using this software as your sole
+method of secure backup.
+
 ### Starting the Server
 
-The dsfxnode requires a listening host, port, and an identity key (ED25519 private key in Base64 format). For example:
+The dsfxnode requires a listening host and port. On it's first run, it will
+attempt to initialize a new folder at the directory specified by the `-dir` flag.
+The default value of this is `$HOME/.dsfxnode`. The server will then generate a
+ED25519 key pair, and begin listening for incoming connections. Your
+private key is stored unencrypted at `<-dir>/key`. Please ensure that
+this file is kept secure. Currently the worst case scenario if this file is lost
+is that you will need to generate a new key pair, and existing connections will
+not recognize your server anymore. You will still have access to all of your data
+once the new key is generated and the server is restarted.
 
 ```sh
-dsfxnode -host localhost -port 8000 -key /path/to/serverkey
+dsfxnode -host localhost -port 8000
 ```
 
-> Note, if you need to generate a new ED25519 key, you can use the following command: `go run ./tool/genkey > path/to/masterkey`
+> Note, if you need to generate a new ED25519 key, you can use the following command: `go run ./tool/genkey > path/to/key`
 
 Command-line flags for dsfx-server:
 
@@ -64,8 +82,12 @@ The host interface on which the server will listen.
 -port (default 8000)
 The TCP port on which the server will accept connections.
 
--key (required)
-File path to the Base64-encoded ED25519 private key that serves as the server’s master key.
+-dir (default "~/.dsfxnode")
+The directory where the server will store files. The default is `$HOME/.dsfxnode`.
+
+-log (default "<-dir>/log")
+The file path where the server will write logs. As a special case, you may run
+`-log stdout` to write logs to standard output.
 
 Once started, the server will bind to the specified host and port and wait for incoming secure file exchange (or other test) connections. When a client connects, the initial payload (up to 1024 bytes) from the client is read and logged.
 
@@ -76,13 +98,15 @@ The dsfxctl uses a private key for the client (also an ED25519 key in Base64 for
 Client command usage:
 
 ```sh
-dsfxctl -key /path/to/clientkey test <remote_addr>
+dsfxctl test <remote_addr>
 ```
 
 Where:
 
--key (required)
-Specifies the file path to your client’s PEM-encoded private key.
+Command-line flags for dsfx-server:
+
+-dir (default "~/.dsfxctl")
+The directory where the client will store files. The default is `$HOME/.dsfxctl`.
 
 The command-line arguments for the dsfx-client are as follows:
 
@@ -91,16 +115,20 @@ Tests the connection against the remote dsfx-server instance.
 
 <remote_addr>:
 The address of the server in the format “dsfx://IP:PORT#PUBLIC_KEY_BASE_64”.
-For example, `dsfx://127.0.0.1:8000#<base64 pubkey>” or “dsfx://127.0.0.1:8000#eyJuIjoiLy8v...`
+For example, `dsfx://127.0.0.1:8000#m8I9H6qf2RLMhwnSHjJAkxq2Zeuv6a+/JDdJB9C6O24=`.
 
 Example:
 
 ```sh
-dsfxctl -key ./dsfx-client/masterkey test dsfx://127.0.0.1:8000#eyJuIjoiLy8v..
+dsfxctl test dsfx://127.0.0.1:8000#m8I9H6qf2RLMhwnSHjJAkxq2Zeuv6a+/JDdJB9C6O24=
 ```
 
 If no command or an unrecognized command is provided, the client will print a brief usage message and exit.
 
+The first time you run the client, it will generate a new ED25519 key pair and
+store it in the file `<-dir>/key`. This key pair is used for all subsequent
+connections to the server.
+
 ### Help and Usage Information
 
 For quick help, simply pass the -h flag:
diff --git a/cmd/dsfxctl/main.go b/cmd/dsfxctl/main.go
index 0cd4cfa..abdbf14 100644
--- a/cmd/dsfxctl/main.go
+++ b/cmd/dsfxctl/main.go
@@ -3,11 +3,14 @@ package main
 import (
 	"context"
 	"crypto/ed25519"
+	"encoding/base64"
+	"errors"
 	"flag"
 	"fmt"
 	"log/slog"
 	"net"
 	"os"
+	"path/filepath"
 
 	"koti.casa/numenor-labs/dsfx/pkg/crypto/identity"
 	"koti.casa/numenor-labs/dsfx/pkg/logging"
@@ -33,9 +36,55 @@ func main() {
 	slog.SetDefault(logger)
 	ctx = logging.WithContext(ctx, logger)
 
+	homedir, err := os.UserHomeDir()
+	if err != nil {
+		logger.ErrorContext(ctx, "failed to get home directory", slog.Any("error", err))
+		os.Exit(1)
+	}
+
+	flagDir := flag.String("dir", filepath.Join(homedir, ".dsfxctl"), "data directory")
+
+	err = os.Mkdir(*flagDir, 0777)
+	if errors.Is(err, os.ErrExist) {
+		// If the directory already exists, we can ignore the error.
+		err = nil
+	}
+	if err != nil {
+		logger.ErrorContext(ctx, "failed to create directory", slog.Any("error", err))
+		os.Exit(1)
+	}
+
+	keyFile, err := os.Open(filepath.Join(*flagDir, "key"))
+	if err != nil {
+		logger.WarnContext(ctx, "key file is missing, reinitializing")
+		logger.WarnContext(ctx, "if this is your first time running dsfxctl, you can ignore this")
+	}
+	if keyFile == nil {
+		logger.InfoContext(ctx, "generating new key")
+		keyFile, err = os.Create(filepath.Join(*flagDir, "key"))
+		if err != nil {
+			logger.ErrorContext(ctx, "failed to create key file", slog.Any("error", err))
+			os.Exit(1)
+		}
+		privkey, err := identity.Generate()
+		if err != nil {
+			logger.ErrorContext(ctx, "failed to generate key", slog.Any("error", err))
+			os.Exit(1)
+		}
+
+		_, err = keyFile.Write([]byte(base64.StdEncoding.EncodeToString(privkey)))
+		if err != nil {
+			logger.ErrorContext(ctx, "failed to write key", slog.Any("error", err))
+			os.Exit(1)
+		}
+	}
+	defer keyFile.Close()
+
 	// ---------------------------------------------------------------------------
 	// Commands
 
+	flag.Parse()
+
 	flag.Usage = func() {
 		fmt.Fprintf(os.Stderr, "Usage: dsfxctl [command] [args]\n")
 		fmt.Fprintf(os.Stderr, "Commands:\n")
@@ -44,16 +93,7 @@ func main() {
 		flag.PrintDefaults()
 	}
 
-	flagKey := flag.String("key", "", "the path to the key file")
-
-	flag.Parse()
-
-	if *flagKey == "" {
-		logger.ErrorContext(ctx, "private key path is required")
-		os.Exit(1)
-	}
-
-	id, err := identity.LoadSigningKeyFromFile(*flagKey)
+	id, err := identity.LoadSigningKeyFromFile(filepath.Join(*flagDir, "key"))
 	if err != nil {
 		logger.ErrorContext(ctx, "failed to load private key", slog.Any("error", err))
 		os.Exit(1)
diff --git a/cmd/dsfxnode/main.go b/cmd/dsfxnode/main.go
index b7e88dc..cb91ccb 100644
--- a/cmd/dsfxnode/main.go
+++ b/cmd/dsfxnode/main.go
@@ -2,34 +2,77 @@ package main
 
 import (
 	"context"
+	"encoding/base64"
+	"errors"
 	"flag"
 	"fmt"
 	"log/slog"
 	"net"
 	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
 
 	"koti.casa/numenor-labs/dsfx/pkg/crypto/identity"
 	"koti.casa/numenor-labs/dsfx/pkg/logging"
 	"koti.casa/numenor-labs/dsfx/pkg/network"
 )
 
-var (
-	flagHost = flag.String("host", "localhost", "the host to listen on")
-	flagPort = flag.Int("port", 8000, "the port to listen on")
-	flagKey  = flag.String("key", "", "the path to the key file")
-)
+var ()
 
 func main() {
 	ctx := context.Background()
 
 	// ---------------------------------------------------------------------------
 	// Logger
+	homedir, err := os.UserHomeDir()
+	if err != nil {
+		logUserMsg("error: failed to get home directory\n")
+		logUserMsg("error: are you on a real computer?\n")
+		// Look up why this function might fail...
+		os.Exit(1)
+	}
+
+	flagHost := flag.String("host", "localhost", "the host to listen on")
+	flagPort := flag.Int("port", 8000, "the port to listen on")
+	flagDir := flag.String("dir", filepath.Join(homedir, ".dsfxnode"), "the directory to store data in")
+	flagLogFile := flag.String("log", "logs", "the file to write logs to, relative to -dir")
+	flag.Parse()
+
+	dataDir := *flagDir
+
+	err = os.Mkdir(dataDir, 0777)
+	if errors.Is(err, os.ErrExist) {
+		// If the directory already exists, we can ignore the error.
+		err = nil
+	}
+	if err != nil {
+		logUserMsg("error: failed to create the data directory: %v\n", err)
+		os.Exit(1)
+	}
+
+	var logFile *os.File
+	if *flagLogFile == "stdout" {
+		logFile = os.Stdout
+	} else {
+		logFilePath := filepath.Join(dataDir, *flagLogFile)
+		logFile, err = os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
+		if err != nil {
+			logUserMsg("warn: log file missing, making a new one: %s\n", logFilePath)
+			logFile, err = os.Create(logFilePath)
+			if err != nil {
+				logUserMsg("error: failed to create log file: %v\n", err)
+				os.Exit(1)
+			}
+		}
+	}
+	defer logFile.Close()
 
 	opts := &slog.HandlerOptions{
 		AddSource: false,
 		Level:     slog.LevelDebug,
 	}
-	logger := slog.New(slog.NewTextHandler(os.Stdout, opts))
+	logger := slog.New(slog.NewTextHandler(logFile, opts))
 
 	// Everything in the application will attempt to use the logger in stored in
 	// the context, but we also set the default with slog as a fallback. In cases
@@ -38,26 +81,71 @@ func main() {
 	slog.SetDefault(logger)
 	ctx = logging.WithContext(ctx, logger)
 
+	keyFile, err := os.Open(filepath.Join(dataDir, "key"))
+	if err != nil {
+		logUserMsg("warn: key file is missing, making a new one\n")
+		logUserMsg("warn: if this is your first time running dsfxctl, you can ignore this\n")
+
+		keyFile, err = os.Create(filepath.Join(dataDir, "key"))
+		if err != nil {
+			logUserMsg("error: failed to create key file: %v\n", err)
+			os.Exit(1)
+		}
+		privkey, err := identity.Generate()
+		if err != nil {
+			logUserMsg("error: failed to generate key: %v\n", err)
+			os.Exit(1)
+		}
+
+		_, err = keyFile.Write([]byte(base64.StdEncoding.EncodeToString(privkey)))
+		if err != nil {
+			logUserMsg("error: failed to write key: %v\n", err)
+			os.Exit(1)
+		}
+	}
+	defer keyFile.Close()
+
 	// ---------------------------------------------------------------------------
 	// Flags
 
-	flag.Parse()
-
-	if *flagKey == "" {
-		slog.ErrorContext(ctx, "master key path is required")
+	masterKey, err := identity.LoadSigningKeyFromFile(filepath.Join(dataDir, "key"))
+	if err != nil {
+		logUserMsg("error: failed to load key: %v\n", err)
 		os.Exit(1)
 	}
 
-	masterKey, err := identity.LoadSigningKeyFromFile(*flagKey)
+	var admins []string
+	adminsFile, err := os.ReadFile(filepath.Join(dataDir, "admins"))
 	if err != nil {
-		logger.ErrorContext(ctx, "failed to load master key", slog.Any("error", err))
-		os.Exit(1)
+		logUserMsg("warn: failed to read admins file.. no one will be able to control this server..\n")
+		logUserMsg(`
+	If you aren't sure what this means, here's a brief explanation:
+	The admins file is a list of public keys that are allowed to control
+	the server. The base are base64 encoded and separated by newlines.
+	The application looks for this file at <dir>/admins, where <dir> is
+	the directory you specified with the -dir flag when starting the
+	system. By default, this is ~/.dsfxnode.
+
+	When you run the dsfxctl command, it will generate a key for you and
+	print it to the console. You can copy this key and paste it into the
+	admins file to give yourself control over the server.
+		`)
+		logUserMsg("\n")
+	}
+	if adminsFile != nil {
+		rawAdmins := strings.Split(string(adminsFile), "\n")
+		for _, admin := range rawAdmins {
+			if admin != "" {
+				admins = append(admins, admin)
+			}
+		}
+		logUserMsg("info: loaded admins: %v\n", admins)
 	}
 
 	tcpAddrRaw := net.JoinHostPort(*flagHost, fmt.Sprint(*flagPort))
 	tcpAddr, err := net.ResolveTCPAddr("tcp", tcpAddrRaw)
 	if err != nil {
-		slog.ErrorContext(ctx, "invalid host or port")
+		logUserMsg("warn: invalid host or port: %v\n", err)
 		os.Exit(1)
 	}
 	addr := network.FromTCPAddr(tcpAddr, identity.ToPublicKey(masterKey))
@@ -71,7 +159,11 @@ func main() {
 		os.Exit(1)
 	}
 
-	logger.InfoContext(ctx, "listener created", slog.String("address", listener.Addr().String()))
+	logUserMsg("info: listener created\n")
+	logUserMsg(">>   Operating System: %s\n", runtime.GOOS)
+	logUserMsg(">>   Architecture: %s\n", runtime.GOARCH)
+	logUserMsg(">>   CPU Cores: %d\n", runtime.NumCPU())
+	logUserMsg(">>   Public Address: %s\n", addr.String())
 
 	for {
 		conn, err := listener.Accept()
@@ -100,3 +192,7 @@ func handleConnection(ctx context.Context, conn net.Conn) error {
 
 	return nil
 }
+
+func logUserMsg(msg string, args ...any) {
+	fmt.Fprintf(os.Stderr, msg, args...)
+}
diff --git a/config.json b/config.json
deleted file mode 100644
index 0970faf..0000000
--- a/config.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
-  "host": "127.0.0.1",
-  "port": 8000,
-  "dataDirectory": "/home/dustin/.dsfx/data",
-  "masterKeyPath": "/home/dustin/.dsfx/masterkey"
-}