sacrifice code quality (temp) for ux

This commit is contained in:
Dustin Stiles 2025-03-10 19:53:10 -04:00
parent 8bfa72fb58
commit 30f031fe1c
Signed by: duwstiles
GPG Key ID: BCD9912EC231FC87
4 changed files with 199 additions and 41 deletions

View File

@ -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 servers 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 clients 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:

View File

@ -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)

View File

@ -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)
// ---------------------------------------------------------------------------
// Flags
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")
flag.Parse()
if *flagKey == "" {
slog.ErrorContext(ctx, "master key path is required")
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)
}
masterKey, err := identity.LoadSigningKeyFromFile(*flagKey)
_, err = keyFile.Write([]byte(base64.StdEncoding.EncodeToString(privkey)))
if err != nil {
logger.ErrorContext(ctx, "failed to load master key", slog.Any("error", err))
logUserMsg("error: failed to write key: %v\n", err)
os.Exit(1)
}
}
defer keyFile.Close()
// ---------------------------------------------------------------------------
// Flags
masterKey, err := identity.LoadSigningKeyFromFile(filepath.Join(dataDir, "key"))
if err != nil {
logUserMsg("error: failed to load key: %v\n", err)
os.Exit(1)
}
var admins []string
adminsFile, err := os.ReadFile(filepath.Join(dataDir, "admins"))
if err != nil {
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...)
}

View File

@ -1,6 +0,0 @@
{
"host": "127.0.0.1",
"port": 8000,
"dataDirectory": "/home/dustin/.dsfx/data",
"masterKeyPath": "/home/dustin/.dsfx/masterkey"
}