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 +dsfxctl test ``` 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. : The address of the server in the format “dsfx://IP:PORT#PUBLIC_KEY_BASE_64”. -For example, `dsfx://127.0.0.1:8000#” 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 /admins, where 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" -}