getsops/sops · error

decrypted plaintext data cannot be cast to string

Error message

decrypted plaintext data cannot be cast to string

What it means

This error is returned by dataKeyFromSecret when secret.Data["plaintext"] exists but is not a Go string. Vault's transit decrypt returns plaintext as a base64 string; any other JSON type breaks the type assertion, so the helper fails fast instead of guessing.

Source

Thrown at hcvault/keysource.go:386

func decryptPayload(encryptedKey string) map[string]interface{} {
	return map[string]interface{}{
		"ciphertext": encryptedKey,
	}
}

// dataKeyFromSecret attempts to extract the data key from the data of the
// provided secret.
func dataKeyFromSecret(secret *api.Secret) ([]byte, error) {
	if secret == nil || secret.Data == nil {
		return nil, fmt.Errorf("transit backend is empty")
	}
	decrypted, ok := secret.Data["plaintext"]
	if !ok {
		return nil, fmt.Errorf("no decrypted data")
	}
	plaintext, ok := decrypted.(string)
	if !ok {
		return nil, fmt.Errorf("decrypted plaintext data cannot be cast to string")
	}
	dataKey, err := base64.StdEncoding.DecodeString(plaintext)
	if err != nil {
		return nil, fmt.Errorf("cannot decode base64 plaintext into data key bytes")
	}
	return dataKey, nil
}

// vaultClient returns a new Vault client, configured with the given address
// and token.
func vaultClient(address, token string, hc *http.Client) (*api.Client, error) {
	cfg := api.DefaultConfig()
	cfg.Address = address

	allowlist, err := getAllowlist()
	if err != nil {
		return nil, err
	}

View on GitHub (pinned to 13442bb981)

Solutions

  1. Inspect the raw decrypt response (`vault write -format=json transit/decrypt/<key> ciphertext=...`) and confirm data.plaintext is a base64 string.
  2. Fix any mock/stub to return {"plaintext":"<base64>"} with a string type.
  3. Upgrade or align the Vault-compatible server so its transit decrypt response matches the standard schema.
  4. Remove proxies/plugins that transform the JSON response between Vault and the client.

Example fix

// before: malformed mock response
{"data":{"plaintext":12345}}
// after: transit-compatible response
{"data":{"plaintext":"c3VwZXJzZWNyZXQ="}}
Defensive patterns

Strategy: type-guard

Validate before calling

raw, ok := secret.Data["plaintext"]
plaintext, isStr := raw.(string)
if !ok || !isStr {
    return fmt.Errorf("unexpected plaintext type %T; transit response schema mismatch", raw)
}
if _, err := base64.StdEncoding.DecodeString(plaintext); err != nil {
    return fmt.Errorf("plaintext is not valid base64")
}

Type guard

func asPlaintextString(secret *api.Secret) (string, bool) {
    if secret == nil || secret.Data == nil { return "", false }
    if v, ok := secret.Data["plaintext"]; ok {
        if s, isStr := v.(string); isStr { return s, true }
    }
    return "", false
}

Try / catch

dataKey, err := dataKeyFromSecret(secret)
if err != nil {
    if strings.Contains(err.Error(), "cannot be cast to string") {
        return fmt.Errorf("vault transit decrypt response schema mismatch (plaintext not a string): %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: DecryptContext (or TestMasterKey_Encrypt) receives a secret whose Data["plaintext"] is a non-string (number, bool, object) — typically from a mocked Vault response, a divergent Vault-compatible server, or middleware altering JSON types.

Common situations: Unit-test stubs returning plaintext as raw bytes/number instead of a base64 string; OpenBao or other Vault forks with changed response encoding; ingress proxies or custom plugins reshaping the decrypt response.

Related errors


AI-assisted analysis of getsops/sops@13442bb981 (2026-09-01). Data as JSON: /api/errors/ca101ff0299ca94c. Report an issue: GitHub.