Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.crewship.ai/llms.txt

Use this file to discover all available pages before exploring further.

Credential Encryption

All credentials in Crewship are encrypted at rest using AES-256-GCM. The implementation in internal/encryption/encryption.go is designed for cross-compatibility between Go and TypeScript.

Encryption Scheme

ParameterValue
AlgorithmAES-256-GCM
Key size256 bits (32 bytes, hex-encoded = 64 chars)
IV (nonce) size16 bytes (not the standard 12 bytes)
Auth tag size16 bytes
Key sourceENCRYPTION_KEY environment variable
Key formatHex-encoded string
The IV/nonce is 16 bytes, not the standard 12 bytes. This is set via cipher.NewGCMWithNonceSize(block, 16) for Go/TypeScript compatibility. Changing this breaks all stored credentials.

Wire Format

Encrypted values are stored as versioned base64 strings:
v1:base64(IV || AuthTag || Ciphertext)

Byte Layout

+--------+----------+------------+
| IV     | AuthTag  | Ciphertext |
| 16 B   | 16 B     | variable   |
+--------+----------+------------+
  bytes    bytes      bytes
  0-15     16-31      32+
This is a custom byte order (IV || AuthTag || Ciphertext), not the standard Go GCM output format (Ciphertext || AuthTag). The reordering is necessary for compatibility with the TypeScript implementation in lib/encryption.ts.
The byte layout is IV(16) || AuthTag(16) || Ciphertext. This differs from Go’s default GCM Seal output which produces Ciphertext || AuthTag. The encryption code explicitly splits and reorders these components. Changing this layout breaks all stored credentials and cross-language compatibility.

Encryption Flow

// internal/encryption/encryption.go - Encrypt function

1. Load key from ENCRYPTION_KEY env var (hex-decoded to 32 bytes)
2. Create AES cipher block
3. Create GCM with 16-byte nonce: cipher.NewGCMWithNonceSize(block, 16)
4. Generate 16 random IV bytes from crypto/rand
5. Seal: sealed = gcm.Seal(nil, iv, plaintext, nil)
   // Go GCM output: ciphertext + authTag (last 16 bytes)
6. Split sealed into ciphertext and authTag
7. Reorder: combined = IV + authTag + ciphertext
8. Encode: "v1:" + base64.StdEncoding.EncodeToString(combined)

Decryption Flow

// internal/encryption/encryption.go - Decrypt function

1. Parse version prefix: "v1:base64data" or legacy "base64data"
2. Look up key for version (ENCRYPTION_KEY or ENCRYPTION_KEY_V2, etc.)
3. Base64 decode (try standard first, fall back to raw/no-padding)
4. Validate minimum length (32 bytes: 16 IV + 16 AuthTag)
5. Extract: iv = data[0:16], authTag = data[16:32], ciphertext = data[32:]
6. Reconstruct Go GCM format: sealed = ciphertext + authTag
7. Decrypt: gcm.Open(nil, iv, sealed, nil)

Key Versioning

Credentials are prefixed with a version identifier (e.g., v1:) to support key rotation:
const currentKeyVersion = "v1"
var versionPattern = regexp.MustCompile(`^v\d+$`)

Key Resolution

VersionEnvironment Variable
v1 (default)ENCRYPTION_KEY
v2ENCRYPTION_KEY_V2
vNENCRYPTION_KEY_VN
If the version-specific env var is not set, it falls back to ENCRYPTION_KEY. This allows gradual key rotation without re-encrypting all existing credentials immediately.

Legacy Format Support

The decryptor accepts both:
  • Versioned: v1:base64data (current format)
  • Legacy: base64data (no version prefix, treated as v1)
Base64 decoding tries standard encoding first, then falls back to raw (no padding) for TypeScript compatibility.

Cross-Language Compatibility

The encryption format is shared between:
  • Go: internal/encryption/encryption.go
  • TypeScript: lib/encryption.ts
Both implementations must agree on:
  1. IV size: 16 bytes
  2. Byte order: IV || AuthTag || Ciphertext
  3. Base64 encoding: standard or raw
  4. Key derivation: direct hex decode (no KDF)

Security Properties

PropertyImplementation
ConfidentialityAES-256-GCM encryption
IntegrityGCM authentication tag
FreshnessRandom 16-byte IV per encryption
Key isolationKey only in environment variable, never in DB
Minimum ciphertext32 bytes (IV + AuthTag with empty plaintext)

Generating an Encryption Key

# Generate a 256-bit key (32 bytes, 64 hex characters)
openssl rand -hex 32

# Example output:
# a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2
The encryption key must be exactly 64 hex characters (32 bytes). A shorter or longer key will cause hex.DecodeString to fail or produce the wrong key length for AES-256.

Credential Lifecycle

User enters credential value
    |
    v
API handler calls encryption.Encrypt(plaintext)
    |
    v
"v1:base64(IV||AuthTag||Ciphertext)" stored in DB
    |
    v (on agent start)
Orchestrator calls encryption.Decrypt(ciphertext)
    |
    v
Plaintext piped to sidecar via stdin JSON
    |
    v
CredStore (in-memory, never on disk)
    |
    v (on HTTP request)
Sidecar injects into outbound request headers
Credentials are transmitted to the sidecar via stdin JSON — not environment variables. This prevents credential leakage through /proc/environ or process listing tools.