feat: implement security, discovery, history, file transfer, web UI (P0-P3)
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
// Package security provides shared-key and TLS certificate generation for
|
||||
// clip-sync peers. It uses only the Go standard library.
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GenerateKey returns a random 256-bit shared key, hex-encoded. It is suitable
|
||||
// for use as the security.shared_key bearer token shared by all peers.
|
||||
func GenerateKey() (string, error) {
|
||||
buf := make([]byte, 32)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return "", fmt.Errorf("generate key: %w", err)
|
||||
}
|
||||
return hex.EncodeToString(buf), nil
|
||||
}
|
||||
|
||||
// GenerateSelfSignedCert creates a self-signed ECDSA certificate and writes it
|
||||
// (PEM-encoded) to certFile and keyFile. It is valid for the given hosts/IPs
|
||||
// and 10 years, which is acceptable for a LAN trust model where all peers
|
||||
// share the same certificate and key files.
|
||||
func GenerateSelfSignedCert(certFile, keyFile string, hosts []string) error {
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate cert: %w", err)
|
||||
}
|
||||
|
||||
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate cert serial: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
tmpl := x509.Certificate{
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{CommonName: "clip-sync"},
|
||||
NotBefore: now.Add(-time.Hour),
|
||||
NotAfter: now.Add(10 * 365 * 24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||
}
|
||||
|
||||
for _, h := range hosts {
|
||||
if ip := net.ParseIP(h); ip != nil {
|
||||
tmpl.IPAddresses = append(tmpl.IPAddresses, ip)
|
||||
} else {
|
||||
tmpl.DNSNames = append(tmpl.DNSNames, h)
|
||||
}
|
||||
}
|
||||
if len(tmpl.IPAddresses) == 0 && len(tmpl.DNSNames) == 0 {
|
||||
tmpl.DNSNames = []string{"localhost"}
|
||||
}
|
||||
|
||||
der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate cert: create: %w", err)
|
||||
}
|
||||
|
||||
certOut, err := os.Create(certFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate cert: %w", err)
|
||||
}
|
||||
defer certOut.Close()
|
||||
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: der}); err != nil {
|
||||
return fmt.Errorf("generate cert: encode: %w", err)
|
||||
}
|
||||
|
||||
keyDER, err := x509.MarshalECPrivateKey(priv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate cert: marshal key: %w", err)
|
||||
}
|
||||
keyOut, err := os.OpenFile(keyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate cert: %w", err)
|
||||
}
|
||||
defer keyOut.Close()
|
||||
if err := pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}); err != nil {
|
||||
return fmt.Errorf("generate cert: encode key: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateKey(t *testing.T) {
|
||||
k1, err := GenerateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKey: %v", err)
|
||||
}
|
||||
k2, err := GenerateKey()
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKey: %v", err)
|
||||
}
|
||||
if len(k1) != 64 {
|
||||
t.Errorf("key length = %d, want 64 hex chars", len(k1))
|
||||
}
|
||||
if k1 == k2 {
|
||||
t.Error("two generated keys are identical")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateSelfSignedCert(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
certFile := filepath.Join(dir, "cert.pem")
|
||||
keyFile := filepath.Join(dir, "key.pem")
|
||||
|
||||
if err := GenerateSelfSignedCert(certFile, keyFile, []string{"localhost", "127.0.0.1"}); err != nil {
|
||||
t.Fatalf("GenerateSelfSignedCert: %v", err)
|
||||
}
|
||||
|
||||
for _, f := range []string{certFile, keyFile} {
|
||||
info, err := os.Stat(f)
|
||||
if err != nil {
|
||||
t.Fatalf("Stat %s: %v", f, err)
|
||||
}
|
||||
if info.Size() == 0 {
|
||||
t.Errorf("%s is empty", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user