mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-17 05:13:01 +00:00
- Invalidate plaintext refresh tokens (migration 019) - Add `--sign` flag to `orama build` for rootwallet manifest signing - Add `--ca-fingerprint` TOFU verification for production joins/invites - Save cluster secrets from join (RQLite auth, Olric key, IPFS peers) - Add RQLite auth config fields
604 lines
20 KiB
Go
604 lines
20 KiB
Go
package production
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/DeBrosOfficial/network/pkg/environments/templates"
|
|
"github.com/libp2p/go-libp2p/core/crypto"
|
|
"github.com/libp2p/go-libp2p/core/peer"
|
|
"github.com/multiformats/go-multiaddr"
|
|
)
|
|
|
|
// ConfigGenerator manages generation of node, gateway, and service configs
|
|
type ConfigGenerator struct {
|
|
oramaDir string
|
|
}
|
|
|
|
// NewConfigGenerator creates a new config generator
|
|
func NewConfigGenerator(oramaDir string) *ConfigGenerator {
|
|
return &ConfigGenerator{
|
|
oramaDir: oramaDir,
|
|
}
|
|
}
|
|
|
|
// extractIPFromMultiaddr extracts the IP address from a peer multiaddr
|
|
// Supports IP4, IP6, DNS4, DNS6, and DNSADDR protocols
|
|
// Returns the IP address as a string, or empty string if extraction/resolution fails
|
|
func extractIPFromMultiaddr(multiaddrStr string) string {
|
|
ma, err := multiaddr.NewMultiaddr(multiaddrStr)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
// First, try to extract direct IP address
|
|
var ip net.IP
|
|
var dnsName string
|
|
multiaddr.ForEach(ma, func(c multiaddr.Component) bool {
|
|
switch c.Protocol().Code {
|
|
case multiaddr.P_IP4, multiaddr.P_IP6:
|
|
ip = net.ParseIP(c.Value())
|
|
return false // Stop iteration - found IP
|
|
case multiaddr.P_DNS4, multiaddr.P_DNS6, multiaddr.P_DNSADDR:
|
|
dnsName = c.Value()
|
|
// Continue to check for IP, but remember DNS name as fallback
|
|
}
|
|
return true
|
|
})
|
|
|
|
// If we found a direct IP, return it
|
|
if ip != nil {
|
|
return ip.String()
|
|
}
|
|
|
|
// If we found a DNS name, try to resolve it
|
|
if dnsName != "" {
|
|
if resolvedIPs, err := net.LookupIP(dnsName); err == nil && len(resolvedIPs) > 0 {
|
|
// Prefer IPv4 addresses, but accept IPv6 if that's all we have
|
|
for _, resolvedIP := range resolvedIPs {
|
|
if resolvedIP.To4() != nil {
|
|
return resolvedIP.String()
|
|
}
|
|
}
|
|
// Return first IPv6 address if no IPv4 found
|
|
return resolvedIPs[0].String()
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// inferPeerIP extracts the IP address from peer multiaddrs
|
|
// Iterates through all peers to find a valid IP (supports DNS resolution)
|
|
// Falls back to vpsIP if provided, otherwise returns empty string
|
|
func inferPeerIP(peers []string, vpsIP string) string {
|
|
// Try to extract IP from each peer (in order)
|
|
for _, peer := range peers {
|
|
if ip := extractIPFromMultiaddr(peer); ip != "" {
|
|
return ip
|
|
}
|
|
}
|
|
// Fall back to vpsIP if provided
|
|
if vpsIP != "" {
|
|
return vpsIP
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// GenerateNodeConfig generates node.yaml configuration (unified architecture)
|
|
func (cg *ConfigGenerator) GenerateNodeConfig(peerAddresses []string, vpsIP string, joinAddress string, domain string, baseDomain string, enableHTTPS bool) (string, error) {
|
|
// Generate node ID from domain or use default
|
|
nodeID := "node"
|
|
if domain != "" {
|
|
// Extract node identifier from domain (e.g., "node-123" from "node-123.orama.network")
|
|
parts := strings.Split(domain, ".")
|
|
if len(parts) > 0 {
|
|
nodeID = parts[0]
|
|
}
|
|
}
|
|
|
|
// Determine advertise addresses - use vpsIP if provided
|
|
// Always use port 7001 for RQLite Raft (no TLS)
|
|
var httpAdvAddr, raftAdvAddr string
|
|
if vpsIP != "" {
|
|
httpAdvAddr = net.JoinHostPort(vpsIP, "5001")
|
|
raftAdvAddr = net.JoinHostPort(vpsIP, "7001")
|
|
} else {
|
|
// Fallback to localhost if no vpsIP
|
|
httpAdvAddr = "localhost:5001"
|
|
raftAdvAddr = "localhost:7001"
|
|
}
|
|
|
|
// Determine RQLite join address
|
|
// Always use port 7001 for RQLite Raft communication (no TLS)
|
|
joinPort := "7001"
|
|
|
|
var rqliteJoinAddr string
|
|
if joinAddress != "" {
|
|
// Use explicitly provided join address
|
|
// Normalize to port 7001 (non-TLS) regardless of what was provided
|
|
if strings.Contains(joinAddress, ":7002") {
|
|
rqliteJoinAddr = strings.Replace(joinAddress, ":7002", ":7001", 1)
|
|
} else {
|
|
rqliteJoinAddr = joinAddress
|
|
}
|
|
} else if len(peerAddresses) > 0 {
|
|
// Infer join address from peers
|
|
peerIP := inferPeerIP(peerAddresses, "")
|
|
if peerIP != "" {
|
|
rqliteJoinAddr = net.JoinHostPort(peerIP, joinPort)
|
|
// Validate that join address doesn't match this node's own raft address (would cause self-join)
|
|
if rqliteJoinAddr == raftAdvAddr {
|
|
rqliteJoinAddr = "" // Clear it - this is the first node
|
|
}
|
|
}
|
|
}
|
|
// If no join address and no peers, this is the first node - it will create the cluster
|
|
|
|
// TLS/ACME configuration
|
|
tlsCacheDir := ""
|
|
httpPort := 80
|
|
httpsPort := 443
|
|
if enableHTTPS {
|
|
tlsCacheDir = filepath.Join(cg.oramaDir, "tls-cache")
|
|
}
|
|
|
|
// Unified data directory (all nodes equal)
|
|
// Always use port 7001 for RQLite Raft - TLS is optional and managed separately
|
|
// The SNI gateway approach was removed to simplify certificate management
|
|
raftInternalPort := 7001
|
|
|
|
data := templates.NodeConfigData{
|
|
NodeID: nodeID,
|
|
P2PPort: 4001,
|
|
DataDir: filepath.Join(cg.oramaDir, "data"),
|
|
RQLiteHTTPPort: 5001,
|
|
RQLiteRaftPort: 7001, // External SNI port
|
|
RQLiteRaftInternalPort: raftInternalPort, // Internal RQLite binding port
|
|
RQLiteJoinAddress: rqliteJoinAddr,
|
|
BootstrapPeers: peerAddresses,
|
|
ClusterAPIPort: 9094,
|
|
IPFSAPIPort: 4501,
|
|
HTTPAdvAddress: httpAdvAddr,
|
|
RaftAdvAddress: raftAdvAddr,
|
|
UnifiedGatewayPort: 6001,
|
|
Domain: domain,
|
|
BaseDomain: baseDomain,
|
|
EnableHTTPS: enableHTTPS,
|
|
TLSCacheDir: tlsCacheDir,
|
|
HTTPPort: httpPort,
|
|
HTTPSPort: httpsPort,
|
|
WGIP: vpsIP,
|
|
}
|
|
|
|
// MinClusterSize=1 for all nodes. Joining nodes use the -join flag to
|
|
// connect to the existing cluster; gating on peer discovery caused a
|
|
// deadlock where the WG sync loop (needs RQLite) couldn't add new peers
|
|
// and RQLite (needs WG peers discovered) couldn't start.
|
|
// Solo-bootstrap protection is already handled by performPreStartClusterDiscovery
|
|
// which refuses to write a single-node peers.json.
|
|
data.MinClusterSize = 1
|
|
|
|
// RQLite node-to-node TLS encryption is disabled by default
|
|
// This simplifies certificate management - RQLite uses plain TCP for internal Raft
|
|
// HTTPS is still used for client-facing gateway traffic via autocert
|
|
// TLS can be enabled manually later if needed for inter-node encryption
|
|
|
|
return templates.RenderNodeConfig(data)
|
|
}
|
|
|
|
// GenerateVaultConfig generates vault.yaml configuration for the Vault Guardian.
|
|
// The vault config uses key=value format (not YAML, despite the file extension).
|
|
// Peer discovery is dynamic via RQLite — no static peer list needed.
|
|
func (cg *ConfigGenerator) GenerateVaultConfig(vpsIP string) string {
|
|
dataDir := filepath.Join(cg.oramaDir, "data", "vault")
|
|
|
|
// Bind to WireGuard IP so vault is only accessible over the overlay network.
|
|
// If no WG IP is provided, bind to localhost as a safe default.
|
|
bindAddr := "127.0.0.1"
|
|
if vpsIP != "" {
|
|
bindAddr = vpsIP
|
|
}
|
|
|
|
return fmt.Sprintf(`# Vault Guardian Configuration
|
|
# Generated by orama node install
|
|
|
|
listen_address = %s
|
|
client_port = 7500
|
|
peer_port = 7501
|
|
data_dir = %s
|
|
rqlite_url = http://127.0.0.1:5001
|
|
`, bindAddr, dataDir)
|
|
}
|
|
|
|
// GenerateGatewayConfig generates gateway.yaml configuration
|
|
func (cg *ConfigGenerator) GenerateGatewayConfig(peerAddresses []string, enableHTTPS bool, domain string, olricServers []string) (string, error) {
|
|
tlsCacheDir := ""
|
|
if enableHTTPS {
|
|
tlsCacheDir = filepath.Join(cg.oramaDir, "tls-cache")
|
|
}
|
|
|
|
data := templates.GatewayConfigData{
|
|
ListenPort: 6001,
|
|
BootstrapPeers: peerAddresses,
|
|
OlricServers: olricServers,
|
|
ClusterAPIPort: 9094,
|
|
IPFSAPIPort: 4501,
|
|
EnableHTTPS: enableHTTPS,
|
|
DomainName: domain,
|
|
TLSCacheDir: tlsCacheDir,
|
|
RQLiteDSN: "", // Empty for now, can be configured later
|
|
}
|
|
return templates.RenderGatewayConfig(data)
|
|
}
|
|
|
|
// GenerateOlricConfig generates Olric configuration.
|
|
// Reads the Olric encryption key from secrets if available.
|
|
func (cg *ConfigGenerator) GenerateOlricConfig(serverBindAddr string, httpPort int, memberlistBindAddr string, memberlistPort int, memberlistEnv string, advertiseAddr string, peers []string) (string, error) {
|
|
// Read encryption key from secrets if available
|
|
encryptionKey := ""
|
|
if data, err := os.ReadFile(filepath.Join(cg.oramaDir, "secrets", "olric-encryption-key")); err == nil {
|
|
encryptionKey = strings.TrimSpace(string(data))
|
|
}
|
|
|
|
data := templates.OlricConfigData{
|
|
ServerBindAddr: serverBindAddr,
|
|
HTTPPort: httpPort,
|
|
MemberlistBindAddr: memberlistBindAddr,
|
|
MemberlistPort: memberlistPort,
|
|
MemberlistEnvironment: memberlistEnv,
|
|
MemberlistAdvertiseAddr: advertiseAddr,
|
|
Peers: peers,
|
|
EncryptionKey: encryptionKey,
|
|
}
|
|
return templates.RenderOlricConfig(data)
|
|
}
|
|
|
|
// SecretGenerator manages generation of shared secrets and keys
|
|
type SecretGenerator struct {
|
|
oramaDir string
|
|
}
|
|
|
|
// NewSecretGenerator creates a new secret generator
|
|
func NewSecretGenerator(oramaDir string) *SecretGenerator {
|
|
return &SecretGenerator{
|
|
oramaDir: oramaDir,
|
|
}
|
|
}
|
|
|
|
// ValidateClusterSecret ensures a cluster secret is 32 bytes of hex
|
|
func ValidateClusterSecret(secret string) error {
|
|
secret = strings.TrimSpace(secret)
|
|
if secret == "" {
|
|
return fmt.Errorf("cluster secret cannot be empty")
|
|
}
|
|
if len(secret) != 64 {
|
|
return fmt.Errorf("cluster secret must be 64 hex characters (32 bytes)")
|
|
}
|
|
if _, err := hex.DecodeString(secret); err != nil {
|
|
return fmt.Errorf("cluster secret must be valid hex: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EnsureClusterSecret gets or generates the IPFS Cluster secret
|
|
func (sg *SecretGenerator) EnsureClusterSecret() (string, error) {
|
|
secretPath := filepath.Join(sg.oramaDir, "secrets", "cluster-secret")
|
|
secretDir := filepath.Dir(secretPath)
|
|
|
|
// Ensure secrets directory exists with restricted permissions (0700)
|
|
if err := os.MkdirAll(secretDir, 0700); err != nil {
|
|
return "", fmt.Errorf("failed to create secrets directory: %w", err)
|
|
}
|
|
// Ensure directory permissions are correct even if it already existed
|
|
if err := os.Chmod(secretDir, 0700); err != nil {
|
|
return "", fmt.Errorf("failed to set secrets directory permissions: %w", err)
|
|
}
|
|
|
|
// Try to read existing secret
|
|
if data, err := os.ReadFile(secretPath); err == nil {
|
|
secret := strings.TrimSpace(string(data))
|
|
if len(secret) == 64 {
|
|
if err := ensureSecretFilePermissions(secretPath); err != nil {
|
|
return "", err
|
|
}
|
|
return secret, nil
|
|
}
|
|
}
|
|
|
|
// Generate new secret (32 bytes = 64 hex chars)
|
|
bytes := make([]byte, 32)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", fmt.Errorf("failed to generate cluster secret: %w", err)
|
|
}
|
|
secret := hex.EncodeToString(bytes)
|
|
|
|
// Write and protect
|
|
if err := os.WriteFile(secretPath, []byte(secret), 0600); err != nil {
|
|
return "", fmt.Errorf("failed to save cluster secret: %w", err)
|
|
}
|
|
if err := ensureSecretFilePermissions(secretPath); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return secret, nil
|
|
}
|
|
|
|
// EnsureRQLiteAuth generates the RQLite auth credentials and JSON auth file.
|
|
// Returns (username, password). The auth JSON file is written to secrets/rqlite-auth.json.
|
|
func (sg *SecretGenerator) EnsureRQLiteAuth() (string, string, error) {
|
|
passwordPath := filepath.Join(sg.oramaDir, "secrets", "rqlite-password")
|
|
authFilePath := filepath.Join(sg.oramaDir, "secrets", "rqlite-auth.json")
|
|
secretDir := filepath.Dir(passwordPath)
|
|
username := "orama"
|
|
|
|
if err := os.MkdirAll(secretDir, 0700); err != nil {
|
|
return "", "", fmt.Errorf("failed to create secrets directory: %w", err)
|
|
}
|
|
if err := os.Chmod(secretDir, 0700); err != nil {
|
|
return "", "", fmt.Errorf("failed to set secrets directory permissions: %w", err)
|
|
}
|
|
|
|
// Try to read existing password
|
|
var password string
|
|
if data, err := os.ReadFile(passwordPath); err == nil {
|
|
password = strings.TrimSpace(string(data))
|
|
}
|
|
|
|
// Generate new password if needed
|
|
if password == "" {
|
|
bytes := make([]byte, 32)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", "", fmt.Errorf("failed to generate RQLite password: %w", err)
|
|
}
|
|
password = hex.EncodeToString(bytes)
|
|
|
|
if err := os.WriteFile(passwordPath, []byte(password), 0600); err != nil {
|
|
return "", "", fmt.Errorf("failed to save RQLite password: %w", err)
|
|
}
|
|
if err := ensureSecretFilePermissions(passwordPath); err != nil {
|
|
return "", "", err
|
|
}
|
|
}
|
|
|
|
// Always regenerate the auth JSON file to ensure consistency
|
|
authJSON := fmt.Sprintf(`[{"username": "%s", "password": "%s", "perms": ["all"]}]`, username, password)
|
|
if err := os.WriteFile(authFilePath, []byte(authJSON), 0600); err != nil {
|
|
return "", "", fmt.Errorf("failed to save RQLite auth file: %w", err)
|
|
}
|
|
if err := ensureSecretFilePermissions(authFilePath); err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
return username, password, nil
|
|
}
|
|
|
|
// EnsureOlricEncryptionKey gets or generates a 32-byte encryption key for Olric memberlist gossip.
|
|
// The key is stored as base64 on disk and returned as base64 (what Olric expects).
|
|
func (sg *SecretGenerator) EnsureOlricEncryptionKey() (string, error) {
|
|
secretPath := filepath.Join(sg.oramaDir, "secrets", "olric-encryption-key")
|
|
secretDir := filepath.Dir(secretPath)
|
|
|
|
if err := os.MkdirAll(secretDir, 0700); err != nil {
|
|
return "", fmt.Errorf("failed to create secrets directory: %w", err)
|
|
}
|
|
if err := os.Chmod(secretDir, 0700); err != nil {
|
|
return "", fmt.Errorf("failed to set secrets directory permissions: %w", err)
|
|
}
|
|
|
|
// Try to read existing key
|
|
if data, err := os.ReadFile(secretPath); err == nil {
|
|
key := strings.TrimSpace(string(data))
|
|
if key != "" {
|
|
if err := ensureSecretFilePermissions(secretPath); err != nil {
|
|
return "", err
|
|
}
|
|
return key, nil
|
|
}
|
|
}
|
|
|
|
// Generate new 32-byte key, base64 encoded
|
|
keyBytes := make([]byte, 32)
|
|
if _, err := rand.Read(keyBytes); err != nil {
|
|
return "", fmt.Errorf("failed to generate Olric encryption key: %w", err)
|
|
}
|
|
key := base64.StdEncoding.EncodeToString(keyBytes)
|
|
|
|
if err := os.WriteFile(secretPath, []byte(key), 0600); err != nil {
|
|
return "", fmt.Errorf("failed to save Olric encryption key: %w", err)
|
|
}
|
|
if err := ensureSecretFilePermissions(secretPath); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
|
|
// EnsureAPIKeyHMACSecret gets or generates the HMAC secret used to hash API keys.
|
|
// The secret is a 32-byte random value stored as 64 hex characters.
|
|
func (sg *SecretGenerator) EnsureAPIKeyHMACSecret() (string, error) {
|
|
secretPath := filepath.Join(sg.oramaDir, "secrets", "api-key-hmac-secret")
|
|
secretDir := filepath.Dir(secretPath)
|
|
|
|
if err := os.MkdirAll(secretDir, 0700); err != nil {
|
|
return "", fmt.Errorf("failed to create secrets directory: %w", err)
|
|
}
|
|
if err := os.Chmod(secretDir, 0700); err != nil {
|
|
return "", fmt.Errorf("failed to set secrets directory permissions: %w", err)
|
|
}
|
|
|
|
// Try to read existing secret
|
|
if data, err := os.ReadFile(secretPath); err == nil {
|
|
secret := strings.TrimSpace(string(data))
|
|
if len(secret) == 64 {
|
|
if err := ensureSecretFilePermissions(secretPath); err != nil {
|
|
return "", err
|
|
}
|
|
return secret, nil
|
|
}
|
|
}
|
|
|
|
// Generate new secret (32 bytes = 64 hex chars)
|
|
bytes := make([]byte, 32)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", fmt.Errorf("failed to generate API key HMAC secret: %w", err)
|
|
}
|
|
secret := hex.EncodeToString(bytes)
|
|
|
|
if err := os.WriteFile(secretPath, []byte(secret), 0600); err != nil {
|
|
return "", fmt.Errorf("failed to save API key HMAC secret: %w", err)
|
|
}
|
|
if err := ensureSecretFilePermissions(secretPath); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return secret, nil
|
|
}
|
|
|
|
func ensureSecretFilePermissions(secretPath string) error {
|
|
if err := os.Chmod(secretPath, 0600); err != nil {
|
|
return fmt.Errorf("failed to set permissions on %s: %w", secretPath, err)
|
|
}
|
|
|
|
if usr, err := user.Lookup("orama"); err == nil {
|
|
uid, err := strconv.Atoi(usr.Uid)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse orama UID: %w", err)
|
|
}
|
|
gid, err := strconv.Atoi(usr.Gid)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse orama GID: %w", err)
|
|
}
|
|
if err := os.Chown(secretPath, uid, gid); err != nil {
|
|
return fmt.Errorf("failed to change ownership of %s: %w", secretPath, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// EnsureSwarmKey gets or generates the IPFS private swarm key
|
|
func (sg *SecretGenerator) EnsureSwarmKey() ([]byte, error) {
|
|
swarmKeyPath := filepath.Join(sg.oramaDir, "secrets", "swarm.key")
|
|
secretDir := filepath.Dir(swarmKeyPath)
|
|
|
|
// Ensure secrets directory exists with restricted permissions (0700)
|
|
if err := os.MkdirAll(secretDir, 0700); err != nil {
|
|
return nil, fmt.Errorf("failed to create secrets directory: %w", err)
|
|
}
|
|
// Ensure directory permissions are correct even if it already existed
|
|
if err := os.Chmod(secretDir, 0700); err != nil {
|
|
return nil, fmt.Errorf("failed to set secrets directory permissions: %w", err)
|
|
}
|
|
|
|
// Try to read existing key — validate and auto-fix if corrupted (e.g. double headers)
|
|
if data, err := os.ReadFile(swarmKeyPath); err == nil {
|
|
content := string(data)
|
|
if strings.Contains(content, "/key/swarm/psk/1.0.0/") {
|
|
// Extract hex and rebuild clean file
|
|
lines := strings.Split(strings.TrimSpace(content), "\n")
|
|
hexKey := ""
|
|
for i := len(lines) - 1; i >= 0; i-- {
|
|
line := strings.TrimSpace(lines[i])
|
|
if line != "" && !strings.HasPrefix(line, "/") {
|
|
hexKey = line
|
|
break
|
|
}
|
|
}
|
|
clean := fmt.Sprintf("/key/swarm/psk/1.0.0/\n/base16/\n%s\n", hexKey)
|
|
if clean != content {
|
|
_ = os.WriteFile(swarmKeyPath, []byte(clean), 0600)
|
|
}
|
|
return []byte(clean), nil
|
|
}
|
|
}
|
|
|
|
// Generate new key (32 bytes)
|
|
keyBytes := make([]byte, 32)
|
|
if _, err := rand.Read(keyBytes); err != nil {
|
|
return nil, fmt.Errorf("failed to generate swarm key: %w", err)
|
|
}
|
|
|
|
keyHex := strings.ToUpper(hex.EncodeToString(keyBytes))
|
|
content := fmt.Sprintf("/key/swarm/psk/1.0.0/\n/base16/\n%s\n", keyHex)
|
|
|
|
// Write and protect
|
|
if err := os.WriteFile(swarmKeyPath, []byte(content), 0600); err != nil {
|
|
return nil, fmt.Errorf("failed to save swarm key: %w", err)
|
|
}
|
|
|
|
return []byte(content), nil
|
|
}
|
|
|
|
// EnsureNodeIdentity gets or generates the node's LibP2P identity (unified - no bootstrap/node distinction)
|
|
func (sg *SecretGenerator) EnsureNodeIdentity() (peer.ID, error) {
|
|
// Unified data directory (no bootstrap/node distinction)
|
|
keyDir := filepath.Join(sg.oramaDir, "data")
|
|
keyPath := filepath.Join(keyDir, "identity.key")
|
|
|
|
// Ensure data directory exists
|
|
if err := os.MkdirAll(keyDir, 0755); err != nil {
|
|
return "", fmt.Errorf("failed to create data directory: %w", err)
|
|
}
|
|
|
|
// Try to read existing key
|
|
if data, err := os.ReadFile(keyPath); err == nil {
|
|
priv, err := crypto.UnmarshalPrivateKey(data)
|
|
if err == nil {
|
|
pub := priv.GetPublic()
|
|
peerID, _ := peer.IDFromPublicKey(pub)
|
|
return peerID, nil
|
|
}
|
|
}
|
|
|
|
// Generate new identity
|
|
priv, pub, err := crypto.GenerateKeyPair(crypto.Ed25519, 2048)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to generate identity: %w", err)
|
|
}
|
|
|
|
peerID, _ := peer.IDFromPublicKey(pub)
|
|
|
|
// Marshal and save private key
|
|
keyData, err := crypto.MarshalPrivateKey(priv)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to marshal private key: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(keyPath, keyData, 0600); err != nil {
|
|
return "", fmt.Errorf("failed to save identity key: %w", err)
|
|
}
|
|
|
|
return peerID, nil
|
|
}
|
|
|
|
// SaveConfig writes a configuration file to disk
|
|
func (sg *SecretGenerator) SaveConfig(filename string, content string) error {
|
|
var configDir string
|
|
// gateway.yaml goes to data/ directory, other configs go to configs/
|
|
if filename == "gateway.yaml" {
|
|
configDir = filepath.Join(sg.oramaDir, "data")
|
|
} else {
|
|
configDir = filepath.Join(sg.oramaDir, "configs")
|
|
}
|
|
|
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create config directory: %w", err)
|
|
}
|
|
|
|
configPath := filepath.Join(configDir, filename)
|
|
if err := os.WriteFile(configPath, []byte(content), 0644); err != nil {
|
|
return fmt.Errorf("failed to write config %s: %w", filename, err)
|
|
}
|
|
|
|
return nil
|
|
}
|