Skip to content

feat: CipherStash Go Encryption SDK v2 - full feature parity#5

Open
calvinbrewer wants to merge 5 commits intomainfrom
feat/encryption-sdk-v2
Open

feat: CipherStash Go Encryption SDK v2 - full feature parity#5
calvinbrewer wants to merge 5 commits intomainfrom
feat/encryption-sdk-v2

Conversation

@calvinbrewer
Copy link
Copy Markdown
Contributor

Summary

  • Rust FFI upgrade: cipherstash-client 0.24.0 -> 0.34.1-alpha.2 with AutoStrategy, ZeroKMSBuilder, unified EqlCiphertext format, and GoPlaintext multi-type support
  • Query encryption: EncryptQuery / EncryptQueryBulk for searching encrypted columns (equality, full-text, range, JSON containment)
  • Model interface: EncryptModel / DecryptModel / BulkEncryptModels / BulkDecryptModels using cs struct tags for automatic field-level encryption
  • Multi-tenant keysets: KeysetConfig on ClientOpts for per-tenant cryptographic isolation
  • Multi-type plaintext: Encrypt strings, numbers, booleans, and JSON (was string-only)
  • Structured errors: EncryptionError with ErrorCode for programmatic error handling
  • IsEncrypted validation: Standalone function to check if a value is encrypted
  • Renamed: "CipherStash Go Encryption SDK" (was "Protect.go")

Breaking changes

  • Encrypted struct: removed Kind field, unified format
  • Plaintext fields: string -> interface{}
  • Decrypt return: string -> interface{}
  • DecryptOptions.Ciphertext: string -> *Encrypted
  • BulkDecryptPayload.Ciphertext: string -> *Encrypted
  • CastAs constants: renamed (big_int -> bigint, real/double -> number, jsonb -> json, added string)
  • Errors: errors.New -> *EncryptionError with error codes

Test plan

  • Rust: 71 tests passing (query ops, index lookup, type inference, lock context grouping, is_encrypted, encrypt config validation)
  • Go: 48 tests passing (30 protect + 18 model — struct tag parsing, type coercion, error codes, JSON serialization)
  • CI: precompiled libraries need rebuilding for all 6 platforms (this PR triggers the build workflow)
  • Integration test with real CipherStash credentials

🤖 Generated with Claude Code

calvinbrewer and others added 5 commits April 16, 2026 06:42
Upgrade the Rust FFI layer from cipherstash-client 0.24.0 to 0.34.1-alpha.2
and add query encryption, model interface, multi-tenant keysets, multi-type
plaintext support, and structured error handling.

Rust FFI changes:
- Upgrade to cipherstash-client 0.34.1-alpha.2 with AutoStrategy/ZeroKMSBuilder
- Replace discriminated Encrypted enum with unified EqlCiphertext format
- Add GoPlaintext multi-type support (string, number, boolean, JSON)
- Add encrypt_query and encrypt_query_bulk FFI functions
- Add is_encrypted validation function
- Add lock_context grouping for bulk operations via BTreeMap
- Add keyset support for multi-tenant encryption
- Add SteVec validation (requires cast_as: json)
- Port query helpers: index lookup, query op parsing, type inference
- 71 Rust tests passing

Go SDK changes:
- Add EncryptQuery/EncryptQueryBulk for searchable encryption queries
- Add model interface (EncryptModel/DecryptModel/BulkEncryptModels/BulkDecryptModels)
- Add IsEncrypted standalone validation function
- Add KeysetConfig for multi-tenant keyset support
- Add EncryptionError with structured error codes
- Update Plaintext to interface{} for multi-type support
- Update Decrypt to return interface{} instead of string
- Update DecryptOptions.Ciphertext to accept *Encrypted instead of raw string
- Update CastAs constants to match new schema (string, number, json)
- Remove Kind field from Encrypted struct (unified format)
- Rename to "CipherStash Go Encryption SDK" in README
- 48 Go tests passing (30 protect + 18 model)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Define encryption schemas using Go struct tags instead of manually
constructing nested EncryptConfig maps. The `cs` tag now carries
the full schema: column name, cast type, and index directives.

Example:
  type User struct {
      ID    int    `json:"id"`
      Email string `json:"email" cs:"email,unique(downcase),match"`
      Age   int    `json:"age"   cs:"age,cast=number,ore"`
  }
  config := protect.BuildEncryptConfig(protect.TableSchema("users", User{}))

Features:
- TableSchema() parses cs tags into TableDef with columns and indexes
- BuildEncryptConfig() assembles multiple TableDefs into EncryptConfig
- Type inference: string→string, int/float→number, bool→boolean, else→json
- Match defaults: ngram(3), downcase, k=6, m=2048, include_original=true
- SteVec auto-sets cast_as to json
- Parenthesized params: match(k=8,m=1024), ste_vec(prefix=t/c)
- Updated analyzeStruct to extract column name from extended cs tags

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restructure the README to lead with the struct tag-based schema builder
as the primary interface. Update the example to demonstrate the complete
SDK workflow: schema definition, model encryption, query encryption,
bulk operations, identity-aware encryption, and structured error handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Complete rewrite of the public API surface to follow Go conventions.
The schema is now the primary API object — column references flow from
it, eliminating raw string table/column arguments at call sites.

API changes:
- Schema-driven: `users.Column("email")` provides ColumnRef, no strings
- context.Context on all I/O methods (Encrypt, Decrypt, EncryptQuery, etc.)
- Functional options: WithSchemas(), WithCredentials(), WithKeyset(),
  WithLockContext(), WithServiceToken(), WithAuditContext()
- Close() implementing io.Closer (was Free())
- NewClient(ctx, ...ClientOption) replaces NewClient(NewClientOptions{})
- Sentinel errors with errors.Is() support (ErrUnknownColumn, etc.)
- Programmatic schema builder: NewSchema().Column().Equality().Done().Build()
- TableSchema() returns (*TableDef, error) instead of panicking
- `any` replaces `interface{}` throughout
- QueryType constants: Equality, FreeTextSearch, OrderAndRange, JSONSelector, JSONContains
- EncryptConfig hidden from public API — built internally from schemas
- DecryptResult.Err is error type, not *string
- Encrypted: []string/[]uint16 instead of *[]string/*[]uint16

Before:
  client, _ := protect.NewClient(protect.NewClientOptions{EncryptConfig: config})
  defer client.Free()
  enc, _ := client.Encrypt(protect.EncryptOptions{Plaintext: "x", Table: "users", Column: "email"})

After:
  client, _ := protect.NewClient(ctx, protect.WithSchemas(users))
  defer client.Close()
  enc, _ := client.Encrypt(ctx, users.Column("email"), "x")

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Audit and fix all production-readiness issues:

Concurrency safety:
- Add sync.RWMutex to Client for goroutine-safe Encrypt/Decrypt/Close
- acquirePtr() takes read lock; Close() takes write lock
- Document Client as safe for concurrent use
- var _ io.Closer = (*Client)(nil) compile-time assertion

Error quality:
- All JSON marshal/unmarshal errors wrapped with operation context
- All context.Err() wrapped with "protect: <Op>:" prefix
- Add ErrFFI sentinel for unrecognized FFI errors (inferSentinel fallback)
- Model methods check for closed client before doing reflection work

Naming:
- CastAsJson → CastAsJSON (Go initialism convention)
- CastAsJson kept as deprecated alias for backward compat

API completeness:
- Add ColumnOK(name) (ColumnRef, bool) — non-panicking alternative to Column()
- Add IsEncrypted CGO cost documentation
- Add package-level Concurrency and Credentials documentation
- Document Decrypt return types (string, float64, bool, map/slice)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant