Skip to content
Open
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
2 changes: 2 additions & 0 deletions cmd/secrets/common/browser_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ func digestHexString(digest [32]byte) string {
// exchanges the code for a short-lived vault JWT, and POSTs the same JSON-RPC body to the gateway with Bearer auth.
// Login tokens in ~/.cre/cre.yaml are not modified; that session stays separate from this vault-only token.
func (h *Handler) executeBrowserUpsert(ctx context.Context, inputs UpsertSecretsInputs, method string) error {
defer ZeroUpsertSecretValues(inputs)

Comment on lines +66 to +67
if h.Credentials.AuthType == credentials.AuthTypeApiKey {
return fmt.Errorf("this sign-in flow requires an interactive login; API keys are not supported")
}
Expand Down
58 changes: 51 additions & 7 deletions cmd/secrets/common/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package common
import (
"context"
"crypto/ecdsa"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -48,10 +49,17 @@ type UpsertSecretsInputs []SecretItem
// SecretItem represents a single secret with its ID, value, and optional namespace.
type SecretItem struct {
ID string `json:"id" validate:"required"`
Value string `json:"value" validate:"required"`
Value []byte `json:"value" validate:"required"`
Namespace string `json:"namespace"`
}

// ZeroUpsertSecretValues overwrites secret payloads in memory.
func ZeroUpsertSecretValues(inputs UpsertSecretsInputs) {
for i := range inputs {
clear(inputs[i].Value)
}
}

type SecretsYamlConfig struct {
SecretsNames map[string][]string `yaml:"secretsNames"`
}
Expand Down Expand Up @@ -161,13 +169,13 @@ func (h *Handler) ResolveInputs() (UpsertSecretsInputs, error) {
if !ok {
return nil, fmt.Errorf("environment variable %q for secret %q not found; please export it", envName, id)
}
if !utf8.ValidString(envVal) {
if !utf8.Valid([]byte(envVal)) {
return nil, fmt.Errorf("value for secret %q (env %q) contains invalid UTF-8", id, envName)
}

out = append(out, SecretItem{
ID: id,
Value: envVal,
Value: []byte(envVal),
Namespace: "main",
})

Expand Down Expand Up @@ -318,8 +326,10 @@ func (h *Handler) EncryptSecrets(rawSecrets UpsertSecretsInputs, owner string) (
}

encryptedSecrets := make([]*vault.EncryptedSecret, 0, len(rawSecrets))
for _, item := range rawSecrets {
for i := range rawSecrets {
item := &rawSecrets[i]
cipherHex, err := EncryptSecret(item.Value, pubKeyHex, owner)
clear(item.Value)
if err != nil {
Comment on lines 328 to 333
return nil, fmt.Errorf("failed to encrypt secret (key=%s ns=%s): %w", item.ID, item.Namespace, err)
}
Expand All @@ -336,8 +346,40 @@ func (h *Handler) EncryptSecrets(rawSecrets UpsertSecretsInputs, owner string) (
return encryptedSecrets, nil
}

// EncryptSecretsForBrowserOrg encrypts secrets scoped to the signed-in organization (interactive sign-in flow).
// TDH2 label is SHA256(orgID); SecretIdentifier.Owner is the org id string. This is a separate binding from the
// owner-key path (EOA left-padded label + workflow owner address); both remain supported via their respective entrypoints.
func (h *Handler) EncryptSecretsForBrowserOrg(rawSecrets UpsertSecretsInputs, orgID string) ([]*vault.EncryptedSecret, error) {
pubKeyHex, err := h.fetchVaultMasterPublicKeyHex()
if err != nil {
return nil, err
}

label := sha256.Sum256([]byte(orgID))

encryptedSecrets := make([]*vault.EncryptedSecret, 0, len(rawSecrets))
for i := range rawSecrets {
item := &rawSecrets[i]
cipherHex, err := encryptSecretWithLabel(item.Value, pubKeyHex, label)
clear(item.Value)
if err != nil {
return nil, fmt.Errorf("failed to encrypt secret (key=%s ns=%s): %w", item.ID, item.Namespace, err)
}
secID := &vault.SecretIdentifier{
Key: item.ID,
Namespace: item.Namespace,
Owner: orgID,
}
encryptedSecrets = append(encryptedSecrets, &vault.EncryptedSecret{
Id: secID,
EncryptedValue: cipherHex,
})
}
return encryptedSecrets, nil
}

// encryptSecretWithLabel encrypts a secret using the vault master public key and the given label.
func encryptSecretWithLabel(secret, masterPublicKeyHex string, label [32]byte) (string, error) {
func encryptSecretWithLabel(secret []byte, masterPublicKeyHex string, label [32]byte) (string, error) {
masterPublicKey := tdh2easy.PublicKey{}
masterPublicKeyBytes, err := hex.DecodeString(masterPublicKeyHex)
if err != nil {
Expand All @@ -347,7 +389,7 @@ func encryptSecretWithLabel(secret, masterPublicKeyHex string, label [32]byte) (
return "", fmt.Errorf("failed to unmarshal master public key: %w", err)
}

cipher, err := tdh2easy.EncryptWithLabel(&masterPublicKey, []byte(secret), label)
cipher, err := tdh2easy.EncryptWithLabel(&masterPublicKey, secret, label)
if err != nil {
return "", fmt.Errorf("failed to encrypt secret: %w", err)
}
Expand All @@ -359,7 +401,7 @@ func encryptSecretWithLabel(secret, masterPublicKeyHex string, label [32]byte) (
}

// EncryptSecret encrypts for the owner-key / web3 flow using a 32-byte label derived from the EOA (12 zero bytes + 20-byte address).
func EncryptSecret(secret, masterPublicKeyHex string, ownerAddress string) (string, error) {
func EncryptSecret(secret []byte, masterPublicKeyHex string, ownerAddress string) (string, error) {
addr := common.HexToAddress(ownerAddress) // canonical 20-byte address
var label [32]byte
copy(label[12:], addr.Bytes()) // left-pad with 12 zero bytes
Expand Down Expand Up @@ -410,6 +452,8 @@ func (h *Handler) Execute(
duration time.Duration,
secretsAuth string,
) error {
defer ZeroUpsertSecretValues(inputs)

h.execCtx = ctx
if IsBrowserFlow(secretsAuth) {
return h.executeBrowserUpsert(ctx, inputs, method)
Expand Down
10 changes: 5 additions & 5 deletions cmd/secrets/common/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ func TestEncryptSecrets(t *testing.T) {
}

raw := UpsertSecretsInputs{
{ID: "test-secret-1", Value: "value1", Namespace: "ns1"},
{ID: "test-secret-2", Value: "another-value", Namespace: "ns2"},
{ID: "test-secret-1", Value: []byte("value1"), Namespace: "ns1"},
{ID: "test-secret-2", Value: []byte("another-value"), Namespace: "ns2"},
}

enc, err := h.EncryptSecrets(raw, "0xabc")
Expand Down Expand Up @@ -102,7 +102,7 @@ func TestEncryptSecrets(t *testing.T) {
},
}

enc, err := h.EncryptSecrets(UpsertSecretsInputs{{ID: "s", Value: "v", Namespace: "n"}}, "0xabc")
enc, err := h.EncryptSecrets(UpsertSecretsInputs{{ID: "s", Value: []byte("v"), Namespace: "n"}}, "0xabc")
require.Error(t, err)
require.Nil(t, enc)
require.Contains(t, err.Error(), "gateway POST failed")
Expand All @@ -128,7 +128,7 @@ func TestEncryptSecrets(t *testing.T) {
},
}

enc, err := h.EncryptSecrets(UpsertSecretsInputs{{ID: "s", Value: "v", Namespace: "n"}}, "0xabc")
enc, err := h.EncryptSecrets(UpsertSecretsInputs{{ID: "s", Value: []byte("v"), Namespace: "n"}}, "0xabc")
require.Error(t, err)
require.Nil(t, enc)
require.Contains(t, err.Error(), "vault public key fetch error")
Expand Down Expand Up @@ -227,7 +227,7 @@ func TestEncryptSecrets_UsesWorkflowOwnerAddress(t *testing.T) {
h.OwnerAddress = "0xabc"

enc, err := h.EncryptSecrets(UpsertSecretsInputs{
{ID: "secret-1", Value: "val1", Namespace: "main"},
{ID: "secret-1", Value: []byte("val1"), Namespace: "main"},
}, "0xabc")
require.NoError(t, err)
require.Len(t, enc, 1)
Expand Down
1 change: 1 addition & 0 deletions cmd/secrets/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func New(ctx *runtime.Context) *cobra.Command {
if err != nil {
return err
}
defer common.ZeroUpsertSecretValues(inputs)

if err := h.ValidateInputs(inputs); err != nil {
return err
Expand Down
1 change: 1 addition & 0 deletions cmd/secrets/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ func New(ctx *runtime.Context) *cobra.Command {
if err != nil {
return err
}
defer common.ZeroUpsertSecretValues(inputs)

if err := h.ValidateInputs(inputs); err != nil {
return err
Expand Down
Loading