Skip to main content
Version: 1.0.0

Secret Types

Safely handle sensitive data with SecretStr and SecretBytes to prevent accidental exposure in logs, API responses, and error messages.

The Problem​

Sensitive data like passwords, API keys, encryption keys, and tokens are frequently exposed by accident:

  • In logs: Developers print full structs with fmt.Printf, revealing secrets
  • In API responses: Marshaling to JSON without masking sensitive fields
  • In error messages: Stack traces or validation errors containing actual values
  • In middleware: Logging frameworks capturing request/response bodies

Pedantigo's secret types automatically mask sensitive data while preserving the actual value for internal use.


SecretStr​

Use SecretStr for string-based secrets: passwords, API keys, tokens, and other sensitive strings.

Overview​

type SecretStr struct {
// value is unexported and private
}

// Create a new secret
func NewSecretStr(s string) SecretStr

// Get the actual secret value
func (s SecretStr) Value() string

// Returns "**********" (safe for logs)
func (s SecretStr) String() string

// Returns "**********" (safe for JSON output)
func (s SecretStr) MarshalJSON() ([]byte, error)

// Stores actual value from JSON input
func (s *SecretStr) UnmarshalJSON(data []byte) error

Behavior​

MethodOutputUse Case
Value()Actual secretInternal processing, passing to external services
String()"**********"fmt.Printf, string concatenation, logging
MarshalJSON()"**********"JSON serialization, API responses
UnmarshalJSON()Stores actual valueJSON deserialization, form input

Example: In Struct Definitions​

type Config struct {
// Database connection string (sensitive)
DatabaseURL SecretStr `json:"database_url" pedantigo:"required"`

// API authentication token
APIToken SecretStr `json:"api_token" pedantigo:"required,min=20"`

// Webhook secret for signature verification
WebhookSecret SecretStr `json:"webhook_secret" pedantigo:"required"`
}

Accessing the Value​

config, err := pedantigo.Unmarshal[Config](jsonData)
if err != nil {
return err
}

// Get the actual secret for use
dbURL := config.DatabaseURL.Value() // Returns: "postgres://user:pass@host/db"

// Connect to database
db, err := sql.Open("postgres", dbURL)

Logging Safety​

When logging structs, the secret is automatically masked:

config := &Config{
DatabaseURL: NewSecretStr("postgres://user:password123@localhost/mydb"),
APIToken: NewSecretStr("sk-1234567890abcdefghijklmn"),
}

// Safe - secrets are masked
fmt.Printf("Config: %v\n", config)
// Output: Config: {DatabaseURL:********** APIToken:**********}

// Also safe - String() method returns masked value
log.Infof("Config loaded with token %s", config.APIToken)
// Log: Config loaded with token **********

API Response Safety​

When marshaling to JSON, secrets are automatically masked:

config := &Config{
DatabaseURL: NewSecretStr("postgres://user:password123@localhost/mydb"),
APIToken: NewSecretStr("sk-1234567890abcdefghijklmn"),
}

// Marshal for API response
jsonData, err := pedantigo.Marshal(config)
if err != nil {
return err
}

// The returned JSON will contain masked values
// {"database_url":"**********","api_token":"**********"}

// Safe to send in HTTP response without data leaks
http.HandleFunc("/api/config", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write(jsonData) // No secrets exposed
})

Complete Example​

package main

import (
"fmt"
"log"
"github.com/smrutai/pedantigo"
)

type AppConfig struct {
// Database credentials
DatabaseURL SecretStr `json:"database_url" pedantigo:"required"`

// External service API key
ExternalAPIKey SecretStr `json:"external_api_key" pedantigo:"required,min=20"`

// Application name (not sensitive)
AppName string `json:"app_name" pedantigo:"required,min=1"`
}

func main() {
// 1. Unmarshal from JSON (actual values stored internally)
jsonInput := []byte(`{
"database_url": "postgres://admin:secretpass@localhost:5432/myapp",
"external_api_key": "sk-1234567890abcdefghijklmnopqrst",
"app_name": "MyApp"
}`)

config, err := pedantigo.Unmarshal[AppConfig](jsonInput)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}

// 2. Log the config - secrets are automatically masked
fmt.Printf("Loaded config: %+v\n", config)
// Output: Loaded config: {DatabaseURL:********** ExternalAPIKey:********** AppName:MyApp}

// 3. Access actual secret value only when needed
dbURL := config.DatabaseURL.Value() // "postgres://admin:secretpass@localhost:5432/myapp"
apiKey := config.ExternalAPIKey.Value() // "sk-1234567890abcdefghijklmnopqrst"

// Use secrets for actual operations
setupDatabase(dbURL)
setupExternalAPI(apiKey)

// 4. Marshal back to JSON - secrets are masked
response, _ := pedantigo.Marshal(config)
fmt.Println(string(response))
// Output: {"database_url":"**********","external_api_key":"**********","app_name":"MyApp"}

// Safe to include in API responses without data leaks
}

func setupDatabase(url string) {
// Use the actual secret
fmt.Printf("Connecting to database at %s\n", url)
}

func setupExternalAPI(key string) {
// Use the actual secret
fmt.Printf("Setting up API with key %s\n", key)
}

SecretBytes​

Use SecretBytes for binary secrets: encryption keys, authentication certificates, and other binary data.

Overview​

type SecretBytes struct {
// value is unexported and private
}

// Create a new secret from bytes
func NewSecretBytes(b []byte) SecretBytes

// Get the actual secret bytes
func (s SecretBytes) Value() []byte

// Returns "**********" (safe for logs)
func (s SecretBytes) String() string

// Returns "**********" (safe for JSON output)
func (s SecretBytes) MarshalJSON() ([]byte, error)

// Expects base64-encoded string in JSON input
func (s *SecretBytes) UnmarshalJSON(data []byte) error

Base64 Encoding for JSON​

Since JSON cannot represent raw binary data, SecretBytes expects base64-encoded strings in JSON input:

type EncryptionConfig struct {
// Encryption key as base64-encoded bytes
EncryptionKey SecretBytes `json:"encryption_key" pedantigo:"required"`
}

// JSON must contain base64-encoded value
jsonInput := []byte(`{
"encryption_key": "aGVsbG8gd29ybGQ=" // base64 for "hello world"
}`)

config, _ := pedantigo.Unmarshal[EncryptionConfig](jsonInput)

// Access actual bytes
key := config.EncryptionKey.Value() // []byte("hello world")

// Marshal back to JSON - masked
output, _ := pedantigo.Marshal(config)
// {"encryption_key":"**********"}

Example: Encryption Key Management​

package main

import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"
"github.com/smrutai/pedantigo"
)

type SecurityConfig struct {
// 32-byte AES-256 encryption key (base64-encoded in JSON)
EncryptionKey SecretBytes `json:"encryption_key" pedantigo:"required"`

// HMAC key for signature verification
HMACKey SecretBytes `json:"hmac_key" pedantigo:"required"`

// Security level
Level string `json:"level" pedantigo:"required,oneof=low medium high"`
}

func main() {
// Generate keys (in real scenarios, load from secure storage)
encKey := make([]byte, 32) // 32 bytes for AES-256
hmacKey := make([]byte, 32)

// Encode to base64 for JSON
encKeyB64 := base64.StdEncoding.EncodeToString(encKey)
hmacKeyB64 := base64.StdEncoding.EncodeToString(hmacKey)

// Create JSON input
jsonInput := []byte(fmt.Sprintf(`{
"encryption_key": "%s",
"hmac_key": "%s",
"level": "high"
}`, encKeyB64, hmacKeyB64))

// Load config
config, err := pedantigo.Unmarshal[SecurityConfig](jsonInput)
if err != nil {
fmt.Printf("Failed to load security config: %v\n", err)
return
}

// 1. Log the config - keys are masked
fmt.Printf("Security config: %+v\n", config)
// Output: Security config: {EncryptionKey:********** HMACKey:********** Level:high}

// 2. Create cipher with actual key
key := config.EncryptionKey.Value() // Get actual 32 bytes
block, _ := aes.NewCipher(key)
cipher := cipher.NewGCM(block)

// Use cipher for encryption/decryption
nonce := make([]byte, cipher.NonceSize())
ciphertext := cipher.Seal(nonce, nonce, []byte("secret data"), nil)
fmt.Printf("Encrypted data: %s\n", base64.StdEncoding.EncodeToString(ciphertext))

// 3. Marshal config - keys are masked
output, _ := pedantigo.Marshal(config)
// Safe to store or transmit: {"encryption_key":"**********","hmac_key":"**********","level":"high"}
}

Complete Integration Example​

This example demonstrates using both SecretStr and SecretBytes together in a real-world scenario:

package main

import (
"crypto/tls"
"database/sql"
"encoding/base64"
"fmt"
"log"
"net/http"
"github.com/smrutai/pedantigo"
)

type ServerConfig struct {
// Server basics
Host string `json:"host" pedantigo:"required"`
Port int `json:"port" pedantigo:"required,min=1,max=65535"`

// Secrets - string-based
JWTSecret SecretStr `json:"jwt_secret" pedantigo:"required,min=32"`
APIKey SecretStr `json:"api_key" pedantigo:"required"`

// Secrets - binary (base64-encoded)
TLSCert SecretBytes `json:"tls_cert" pedantigo:"required"`
TLSKey SecretBytes `json:"tls_key" pedantigo:"required"`

// Non-sensitive fields
Environment string `json:"environment" pedantigo:"required,oneof=dev staging prod"`
Timeout int `json:"timeout" pedantigo:"min=1,max=300"`
}

func main() {
// Load config from JSON (from env file, config server, etc)
configJSON := []byte(`{
"host": "0.0.0.0",
"port": 8443,
"jwt_secret": "your-super-secret-jwt-key-must-be-at-least-32-chars",
"api_key": "sk-1234567890abcdefghijklmnopqrstu",
"tls_cert": "` + base64.StdEncoding.EncodeToString([]byte("cert-data")) + `",
"tls_key": "` + base64.StdEncoding.EncodeToString([]byte("key-data")) + `",
"environment": "prod",
"timeout": 30
}`)

// Parse and validate
config, err := pedantigo.Unmarshal[ServerConfig](configJSON)
if err != nil {
log.Fatalf("Invalid config: %v", err)
}

// Safe to log - secrets are automatically masked
log.Infof("Starting server at %s:%d", config.Host, config.Port)
log.Infof("Config: %+v", config) // Secrets are "**********"

// Set up HTTPS with actual secrets
cert := config.TLSCert.Value() // Get actual certificate bytes
key := config.TLSKey.Value() // Get actual key bytes

tlsCert, err := tls.X509KeyPair(cert, key)
if err != nil {
log.Fatalf("Failed to load TLS credentials: %v", err)
}

// Set up server
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
// Use JWT secret for token validation
jwtSecret := config.JWTSecret.Value()

// Validate request with JWT
token := r.Header.Get("Authorization")
if !validateToken(token, jwtSecret) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}

w.Write([]byte(`{"status":"ok"}`))
})

mux.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) {
// Safe to return config - secrets are masked
configJSON, _ := pedantigo.Marshal(config)
w.Header().Set("Content-Type", "application/json")
w.Write(configJSON)
// Secrets are "**********" - no data leaks!
})

// Start HTTPS server
server := &http.Server{
Addr: fmt.Sprintf("%s:%d", config.Host, config.Port),
Handler: mux,
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{tlsCert},
},
}

log.Fatal(server.ListenAndServeTLS("", ""))
}

func validateToken(token, secret string) bool {
// Token validation using secret
// This is a simplified example
return token != "" && secret != ""
}

Best Practices​

1. Use the Correct Type​

// Good: Use SecretStr for strings
type Config struct {
DatabasePassword SecretStr `json:"db_password"`
}

// Bad: Storing secrets in plain strings
type Config struct {
DatabasePassword string `json:"db_password"` // Will be visible in logs!
}

2. Always Get Value() When Needed​

// Good: Get actual secret only when you need to use it
password := config.DatabasePassword.Value()
db.Connect(password)

// Bad: Logging or printing the struct
log.Println(config) // Even with SecretStr, don't do this carelessly
fmt.Println(config) // Use specific fields only

3. Use Base64 for Binary Secrets​

// Good: Use base64-encoded strings in JSON
type Config struct {
EncryptionKey SecretBytes `json:"encryption_key"` // Expects base64 input
}

// In JSON:
// {"encryption_key": "aGVsbG8gd29ybGQ="}

// Bad: Trying to pass raw bytes (JSON doesn't support)
// {"encryption_key": [104, 101, 108, 108, 111]} // Won't unmarshal properly

4. Combine with Other Constraints​

// Good: Add validation constraints
type Config struct {
APIKey SecretStr `json:"api_key" pedantigo:"required,min=20,max=100"`
Secret SecretStr `json:"secret" pedantigo:"required"`
}

// This ensures:
// - Fields are required
// - APIKey is 20-100 characters
// - Both values are preserved and masked

5. Never Store Masked Values​

// Good: Always work with actual secrets
actual := config.APIKey.Value()

// Bad: Trying to store the masked output
masked := config.APIKey.String() // "**********"
useWithAPI(masked) // This won't work!

Error Handling​

Both SecretStr and SecretBytes report validation errors without exposing secrets:

type Config struct {
APIKey SecretStr `json:"api_key" pedantigo:"required,min=32"`
}

// If validation fails, error message won't contain the actual secret
jsonInput := []byte(`{"api_key": "short"}`)
_, err := pedantigo.Unmarshal[Config](jsonInput)

// Error: validation error: field "api_key": constraint "min" failed
// The actual value "short" is NOT in the error message
log.Println(err) // Safe to log - no secret exposed

Thread Safety​

Both SecretStr and SecretBytes are safe to use concurrently:

var config *ServerConfig
var wg sync.WaitGroup

// Multiple goroutines can access the secrets safely
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()

// All goroutines can call Value() concurrently
secret := config.JWTSecret.Value()
apiKey := config.APIKey.Value()
encKey := config.EncryptionKey.Value()

// Process with secrets
processRequest(secret, apiKey, encKey)
}()
}

wg.Wait()

Summary​

FeatureSecretStrSecretBytes
Use forPasswords, API keys, tokensEncryption keys, certificates
JSON InputPlain stringBase64-encoded string
Value()Returns actual stringReturns actual bytes
String()Returns "**********"Returns "**********"
MarshalJSON()Outputs "**********"Outputs "**********"
Safe for logsYesYes
Safe for JSON outputYesYes
Safe for API responsesYesYes

Both types protect against accidental exposure while preserving full access to the actual secret values when needed.