Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions cmd/wsh/cmd/wshcmd-secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"fmt"
"regexp"
"strings"

"github.com/spf13/cobra"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
)

// secretNameRegex must match the validation in pkg/wconfig/secretstore.go
var secretNameRegex = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]*$`)

var secretCmd = &cobra.Command{
Use: "secret",
Short: "manage secrets",
Long: "Manage secrets for Wave Terminal",
}

var secretGetCmd = &cobra.Command{
Use: "get [name]",
Short: "get a secret value",
Args: cobra.ExactArgs(1),
RunE: secretGetRun,
PreRunE: preRunSetupRpcClient,
}

var secretSetCmd = &cobra.Command{
Use: "set [name]=[value]",
Short: "set a secret value",
Args: cobra.ExactArgs(1),
RunE: secretSetRun,
PreRunE: preRunSetupRpcClient,
}

var secretListCmd = &cobra.Command{
Use: "list",
Short: "list all secret names",
Args: cobra.NoArgs,
RunE: secretListRun,
PreRunE: preRunSetupRpcClient,
}

func init() {
rootCmd.AddCommand(secretCmd)
secretCmd.AddCommand(secretGetCmd)
secretCmd.AddCommand(secretSetCmd)
secretCmd.AddCommand(secretListCmd)
}

func secretGetRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("secret", rtnErr == nil)
}()

name := args[0]
if !secretNameRegex.MatchString(name) {
return fmt.Errorf("invalid secret name: must start with a letter and contain only letters, numbers, and underscores")
}

resp, err := wshclient.GetSecretsCommand(RpcClient, []string{name}, &wshrpc.RpcOpts{Timeout: 2000})
if err != nil {
return fmt.Errorf("getting secret: %w", err)
}

value, ok := resp[name]
if !ok {
return fmt.Errorf("secret not found: %s", name)
}

WriteStdout("%s\n", value)
return nil
}

func secretSetRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("secret", rtnErr == nil)
}()

parts := strings.SplitN(args[0], "=", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid format: expected [name]=[value]")
}

name := parts[0]
value := parts[1]

if name == "" {
return fmt.Errorf("secret name cannot be empty")
}

backend, err := wshclient.GetSecretsLinuxStorageBackendCommand(RpcClient, &wshrpc.RpcOpts{Timeout: 2000})
if err != nil {
return fmt.Errorf("checking secret storage backend: %w", err)
}

if backend == "basic_text" || backend == "unknown" {
return fmt.Errorf("No appropriate secret manager found, cannot set secrets")
}

secrets := map[string]string{name: value}
err = wshclient.SetSecretsCommand(RpcClient, secrets, &wshrpc.RpcOpts{Timeout: 2000})
if err != nil {
return fmt.Errorf("setting secret: %w", err)
}

WriteStdout("secret set: %s\n", name)
return nil
}

func secretListRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("secret", rtnErr == nil)
}()

names, err := wshclient.GetSecretsNamesCommand(RpcClient, &wshrpc.RpcOpts{Timeout: 2000})
if err != nil {
return fmt.Errorf("listing secrets: %w", err)
}

for _, name := range names {
WriteStdout("%s\n", name)
}
return nil
}
44 changes: 43 additions & 1 deletion emain/emain-wsh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { WindowService } from "@/app/store/services";
import { RpcResponseHelper, WshClient } from "@/app/store/wshclient";
import { RpcApi } from "@/app/store/wshclientapi";
import { Notification } from "electron";
import { Notification, safeStorage } from "electron";
import { getResolvedUpdateChannel } from "emain/updater";
import { unamePlatform } from "./emain-platform";
import { getWebContentsByBlockId, webGetSelector } from "./emain-web";
Expand Down Expand Up @@ -60,6 +60,48 @@ export class ElectronWshClientType extends WshClient {
ww.focus();
}

async handle_electronencrypt(
rh: RpcResponseHelper,
data: CommandElectronEncryptData
): Promise<CommandElectronEncryptRtnData> {
if (!safeStorage.isEncryptionAvailable()) {
throw new Error("encryption is not available");
}
const encrypted = safeStorage.encryptString(data.plaintext);
const ciphertext = encrypted.toString("base64");

let storagebackend = "";
if (process.platform === "linux") {
storagebackend = safeStorage.getSelectedStorageBackend();
}

return {
ciphertext,
storagebackend,
};
}

async handle_electrondecrypt(
rh: RpcResponseHelper,
data: CommandElectronDecryptData
): Promise<CommandElectronDecryptRtnData> {
if (!safeStorage.isEncryptionAvailable()) {
throw new Error("encryption is not available");
}
const encrypted = Buffer.from(data.ciphertext, "base64");
const plaintext = safeStorage.decryptString(encrypted);

let storagebackend = "";
if (process.platform === "linux") {
storagebackend = safeStorage.getSelectedStorageBackend();
}

return {
plaintext,
storagebackend,
};
}

// async handle_workspaceupdate(rh: RpcResponseHelper) {
// console.log("workspaceupdate");
// fireAndForget(async () => {
Expand Down
30 changes: 30 additions & 0 deletions frontend/app/store/wshclientapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,16 @@ class RpcApiType {
return client.wshRpcCall("disposesuggestions", data, opts);
}

// command "electrondecrypt" [call]
ElectronDecryptCommand(client: WshClient, data: CommandElectronDecryptData, opts?: RpcOpts): Promise<CommandElectronDecryptRtnData> {
return client.wshRpcCall("electrondecrypt", data, opts);
}

// command "electronencrypt" [call]
ElectronEncryptCommand(client: WshClient, data: CommandElectronEncryptData, opts?: RpcOpts): Promise<CommandElectronEncryptRtnData> {
return client.wshRpcCall("electronencrypt", data, opts);
}

// command "eventpublish" [call]
EventPublishCommand(client: WshClient, data: WaveEvent, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("eventpublish", data, opts);
Expand Down Expand Up @@ -297,6 +307,21 @@ class RpcApiType {
return client.wshRpcCall("getrtinfo", data, opts);
}

// command "getsecrets" [call]
GetSecretsCommand(client: WshClient, data: string[], opts?: RpcOpts): Promise<{[key: string]: string}> {
return client.wshRpcCall("getsecrets", data, opts);
}

// command "getsecretslinuxstoragebackend" [call]
GetSecretsLinuxStorageBackendCommand(client: WshClient, opts?: RpcOpts): Promise<string> {
return client.wshRpcCall("getsecretslinuxstoragebackend", null, opts);
}

// command "getsecretsnames" [call]
GetSecretsNamesCommand(client: WshClient, opts?: RpcOpts): Promise<string[]> {
return client.wshRpcCall("getsecretsnames", null, opts);
}

// command "gettab" [call]
GetTabCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<Tab> {
return client.wshRpcCall("gettab", data, opts);
Expand Down Expand Up @@ -472,6 +497,11 @@ class RpcApiType {
return client.wshRpcCall("setrtinfo", data, opts);
}

// command "setsecrets" [call]
SetSecretsCommand(client: WshClient, data: {[key: string]: string}, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("setsecrets", data, opts);
}

// command "setvar" [call]
SetVarCommand(client: WshClient, data: CommandVarData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("setvar", data, opts);
Expand Down
22 changes: 22 additions & 0 deletions frontend/types/gotypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,28 @@ declare global {
routeid: string;
};

// wshrpc.CommandElectronDecryptData
type CommandElectronDecryptData = {
ciphertext: string;
};

// wshrpc.CommandElectronDecryptRtnData
type CommandElectronDecryptRtnData = {
plaintext: string;
storagebackend: string;
};

// wshrpc.CommandElectronEncryptData
type CommandElectronEncryptData = {
plaintext: string;
};

// wshrpc.CommandElectronEncryptRtnData
type CommandElectronEncryptRtnData = {
ciphertext: string;
storagebackend: string;
};

// wshrpc.CommandEventReadHistoryData
type CommandEventReadHistoryData = {
event: string;
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading