mirror of
https://git.numenor-labs.us/dsfx.git
synced 2025-04-29 08:10:34 +00:00
sacrifice code quality (temp) for ux
This commit is contained in:
parent
8bfa72fb58
commit
30f031fe1c
48
README.md
48
README.md
@ -46,15 +46,33 @@ go install ./cmd/...
|
|||||||
|
|
||||||
## Usage
|
## 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
|
### 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
|
```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:
|
Command-line flags for dsfx-server:
|
||||||
|
|
||||||
@ -64,8 +82,12 @@ The host interface on which the server will listen.
|
|||||||
-port (default 8000)
|
-port (default 8000)
|
||||||
The TCP port on which the server will accept connections.
|
The TCP port on which the server will accept connections.
|
||||||
|
|
||||||
-key (required)
|
-dir (default "~/.dsfxnode")
|
||||||
File path to the Base64-encoded ED25519 private key that serves as the server’s master key.
|
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.
|
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:
|
Client command usage:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
dsfxctl -key /path/to/clientkey test <remote_addr>
|
dsfxctl test <remote_addr>
|
||||||
```
|
```
|
||||||
|
|
||||||
Where:
|
Where:
|
||||||
|
|
||||||
-key (required)
|
Command-line flags for dsfx-server:
|
||||||
Specifies the file path to your client’s PEM-encoded private key.
|
|
||||||
|
-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:
|
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>:
|
<remote_addr>:
|
||||||
The address of the server in the format “dsfx://IP:PORT#PUBLIC_KEY_BASE_64”.
|
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:
|
Example:
|
||||||
|
|
||||||
```sh
|
```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.
|
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
|
### Help and Usage Information
|
||||||
|
|
||||||
For quick help, simply pass the -h flag:
|
For quick help, simply pass the -h flag:
|
||||||
|
@ -3,11 +3,14 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"koti.casa/numenor-labs/dsfx/pkg/crypto/identity"
|
"koti.casa/numenor-labs/dsfx/pkg/crypto/identity"
|
||||||
"koti.casa/numenor-labs/dsfx/pkg/logging"
|
"koti.casa/numenor-labs/dsfx/pkg/logging"
|
||||||
@ -33,9 +36,55 @@ func main() {
|
|||||||
slog.SetDefault(logger)
|
slog.SetDefault(logger)
|
||||||
ctx = logging.WithContext(ctx, 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
|
// Commands
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
flag.Usage = func() {
|
flag.Usage = func() {
|
||||||
fmt.Fprintf(os.Stderr, "Usage: dsfxctl [command] [args]\n")
|
fmt.Fprintf(os.Stderr, "Usage: dsfxctl [command] [args]\n")
|
||||||
fmt.Fprintf(os.Stderr, "Commands:\n")
|
fmt.Fprintf(os.Stderr, "Commands:\n")
|
||||||
@ -44,16 +93,7 @@ func main() {
|
|||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
flagKey := flag.String("key", "", "the path to the key file")
|
id, err := identity.LoadSigningKeyFromFile(filepath.Join(*flagDir, "key"))
|
||||||
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if *flagKey == "" {
|
|
||||||
logger.ErrorContext(ctx, "private key path is required")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := identity.LoadSigningKeyFromFile(*flagKey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ErrorContext(ctx, "failed to load private key", slog.Any("error", err))
|
logger.ErrorContext(ctx, "failed to load private key", slog.Any("error", err))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
@ -2,34 +2,77 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"koti.casa/numenor-labs/dsfx/pkg/crypto/identity"
|
"koti.casa/numenor-labs/dsfx/pkg/crypto/identity"
|
||||||
"koti.casa/numenor-labs/dsfx/pkg/logging"
|
"koti.casa/numenor-labs/dsfx/pkg/logging"
|
||||||
"koti.casa/numenor-labs/dsfx/pkg/network"
|
"koti.casa/numenor-labs/dsfx/pkg/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
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")
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Logger
|
// 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{
|
opts := &slog.HandlerOptions{
|
||||||
AddSource: false,
|
AddSource: false,
|
||||||
Level: slog.LevelDebug,
|
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
|
// 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
|
// the context, but we also set the default with slog as a fallback. In cases
|
||||||
@ -38,26 +81,71 @@ func main() {
|
|||||||
slog.SetDefault(logger)
|
slog.SetDefault(logger)
|
||||||
ctx = logging.WithContext(ctx, logger)
|
ctx = logging.WithContext(ctx, logger)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
keyFile, err := os.Open(filepath.Join(dataDir, "key"))
|
||||||
// Flags
|
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()
|
keyFile, err = os.Create(filepath.Join(dataDir, "key"))
|
||||||
|
if err != nil {
|
||||||
if *flagKey == "" {
|
logUserMsg("error: failed to create key file: %v\n", err)
|
||||||
slog.ErrorContext(ctx, "master key path is required")
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
privkey, err := identity.Generate()
|
||||||
|
if err != nil {
|
||||||
|
logUserMsg("error: failed to generate key: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
masterKey, err := identity.LoadSigningKeyFromFile(*flagKey)
|
_, err = keyFile.Write([]byte(base64.StdEncoding.EncodeToString(privkey)))
|
||||||
if err != nil {
|
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)
|
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))
|
tcpAddrRaw := net.JoinHostPort(*flagHost, fmt.Sprint(*flagPort))
|
||||||
tcpAddr, err := net.ResolveTCPAddr("tcp", tcpAddrRaw)
|
tcpAddr, err := net.ResolveTCPAddr("tcp", tcpAddrRaw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.ErrorContext(ctx, "invalid host or port")
|
logUserMsg("warn: invalid host or port: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
addr := network.FromTCPAddr(tcpAddr, identity.ToPublicKey(masterKey))
|
addr := network.FromTCPAddr(tcpAddr, identity.ToPublicKey(masterKey))
|
||||||
@ -71,7 +159,11 @@ func main() {
|
|||||||
os.Exit(1)
|
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 {
|
for {
|
||||||
conn, err := listener.Accept()
|
conn, err := listener.Accept()
|
||||||
@ -100,3 +192,7 @@ func handleConnection(ctx context.Context, conn net.Conn) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func logUserMsg(msg string, args ...any) {
|
||||||
|
fmt.Fprintf(os.Stderr, msg, args...)
|
||||||
|
}
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"host": "127.0.0.1",
|
|
||||||
"port": 8000,
|
|
||||||
"dataDirectory": "/home/dustin/.dsfx/data",
|
|
||||||
"masterKeyPath": "/home/dustin/.dsfx/masterkey"
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user