// Package unlock implements the genesis node unlock command. // // When the genesis OramaOS node reboots before enough peers exist for // Shamir-based LUKS key reconstruction, the operator must manually provide // the LUKS key. This command reads the encrypted genesis key from the // node's rootfs, decrypts it with the rootwallet, and sends it to the agent. package unlock import ( "bytes" "encoding/base64" "encoding/json" "flag" "fmt" "io" "net/http" "os" "os/exec" "strings" "time" ) // Flags holds parsed command-line flags. type Flags struct { NodeIP string // WireGuard IP of the OramaOS node Genesis bool // Must be set to confirm genesis unlock KeyFile string // Path to the encrypted genesis key file (optional override) } // Handle processes the unlock command. func Handle(args []string) { flags, err := parseFlags(args) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } if !flags.Genesis { fmt.Fprintf(os.Stderr, "Error: --genesis flag is required to confirm genesis unlock\n") os.Exit(1) } // Step 1: Read the encrypted genesis key from the node fmt.Printf("Fetching encrypted genesis key from %s...\n", flags.NodeIP) encKey, err := fetchGenesisKey(flags.NodeIP) if err != nil && flags.KeyFile == "" { fmt.Fprintf(os.Stderr, "Error: could not fetch genesis key from node: %v\n", err) fmt.Fprintf(os.Stderr, "You can provide the key file directly with --key-file\n") os.Exit(1) } if flags.KeyFile != "" { data, readErr := os.ReadFile(flags.KeyFile) if readErr != nil { fmt.Fprintf(os.Stderr, "Error: could not read key file: %v\n", readErr) os.Exit(1) } encKey = strings.TrimSpace(string(data)) } // Step 2: Decrypt with rootwallet fmt.Println("Decrypting genesis key with rootwallet...") luksKey, err := decryptGenesisKey(encKey) if err != nil { fmt.Fprintf(os.Stderr, "Error: decryption failed: %v\n", err) os.Exit(1) } // Step 3: Send LUKS key to the agent over WireGuard fmt.Printf("Sending LUKS key to agent at %s:9998...\n", flags.NodeIP) if err := sendUnlockKey(flags.NodeIP, luksKey); err != nil { fmt.Fprintf(os.Stderr, "Error: unlock failed: %v\n", err) os.Exit(1) } fmt.Println("Genesis node unlocked successfully.") fmt.Println("The node is decrypting and mounting its data partition.") } func parseFlags(args []string) (*Flags, error) { fs := flag.NewFlagSet("unlock", flag.ContinueOnError) fs.SetOutput(os.Stderr) flags := &Flags{} fs.StringVar(&flags.NodeIP, "node-ip", "", "WireGuard IP of the OramaOS node (required)") fs.BoolVar(&flags.Genesis, "genesis", false, "Confirm genesis node unlock") fs.StringVar(&flags.KeyFile, "key-file", "", "Path to encrypted genesis key file (optional)") if err := fs.Parse(args); err != nil { return nil, err } if flags.NodeIP == "" { return nil, fmt.Errorf("--node-ip is required") } return flags, nil } // fetchGenesisKey retrieves the encrypted genesis key from the node. // The agent serves it at GET /v1/agent/genesis-key (only during genesis unlock mode). func fetchGenesisKey(nodeIP string) (string, error) { client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Get(fmt.Sprintf("http://%s:9998/v1/agent/genesis-key", nodeIP)) if err != nil { return "", fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return "", fmt.Errorf("status %d: %s", resp.StatusCode, string(body)) } var result struct { EncryptedKey string `json:"encrypted_key"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return "", fmt.Errorf("invalid response: %w", err) } return result.EncryptedKey, nil } // decryptGenesisKey decrypts the AES-256-GCM encrypted LUKS key using rootwallet. // The key was encrypted with: AES-256-GCM(luksKey, HKDF(rootwalletKey, "genesis-luks")) // For now, we use `rw decrypt` if available, or a local HKDF+AES-GCM implementation. func decryptGenesisKey(encryptedKey string) ([]byte, error) { // Try rw decrypt first cmd := exec.Command("rw", "decrypt", encryptedKey, "--purpose", "genesis-luks", "--chain", "evm") output, err := cmd.Output() if err == nil { decoded, decErr := base64.StdEncoding.DecodeString(strings.TrimSpace(string(output))) if decErr != nil { return nil, fmt.Errorf("failed to decode decrypted key: %w", decErr) } return decoded, nil } return nil, fmt.Errorf("rw decrypt failed: %w (is rootwallet installed and initialized?)", err) } // sendUnlockKey sends the decrypted LUKS key to the agent's unlock endpoint. func sendUnlockKey(nodeIP string, luksKey []byte) error { body, _ := json.Marshal(map[string]string{ "key": base64.StdEncoding.EncodeToString(luksKey), }) client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Post( fmt.Sprintf("http://%s:9998/v1/agent/unlock", nodeIP), "application/json", bytes.NewReader(body), ) if err != nil { return fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { respBody, _ := io.ReadAll(resp.Body) return fmt.Errorf("status %d: %s", resp.StatusCode, string(respBody)) } return nil }