package credentialstore import ( "context" "errors" "fmt" "strings" ) var ( // ErrEnvelopeKeyUnavailable is returned when the key registry cannot be reached or is unconfigured. ErrEnvelopeKeyUnavailable = errors.New("credentialstore: envelope key unavailable") // ErrUnknownEnvelopeKey is returned when the referenced key ID or version is not registered. ErrUnknownEnvelopeKey = errors.New("credentialstore: unknown envelope key") // ErrInvalidSecretEnvelope is returned when envelope fields fail validation. ErrInvalidSecretEnvelope = errors.New("credentialstore: invalid secret envelope") ) // EnvelopeKeyRegistry checks whether an envelope encryption key is registered. type EnvelopeKeyRegistry interface { HasEnvelopeKey(ctx context.Context, keyID string, keyVersion uint64) (bool, error) } const ( // algorithmAES256GCM is the only authenticated-encryption algorithm the // store accepts. It is declared locally so credentialstore never depends on // the higher-layer credentialseal package. algorithmAES256GCM = "AES-256-GCM" // minNonceLength is the minimum AES-GCM nonce length (the 96-bit standard). // Shorter nonces weaken GCM and are rejected at the persistence boundary. minNonceLength = 12 ) type unavailableKeyRegistry struct{} func (unavailableKeyRegistry) HasEnvelopeKey(ctx context.Context, keyID string, keyVersion uint64) (bool, error) { return false, ErrEnvelopeKeyUnavailable } // SecretEnvelope holds opaque encrypted secret payload and key metadata. type SecretEnvelope struct { Algorithm string `json:"algorithm"` KeyID string `json:"key_id"` KeyVersion uint64 `json:"key_version"` Nonce []byte `json:"nonce"` Ciphertext []byte `json:"ciphertext"` AAD []byte `json:"aad,omitempty"` } // Validate checks that the envelope carries a supported authenticated-encryption // algorithm, a registered-shaped key revision, a full-length nonce, and // ciphertext. It stays secret-blind: it inspects only opaque metadata and never // the plaintext. AAD is optional metadata and is not required to be present. func (e SecretEnvelope) Validate() error { algorithm := strings.TrimSpace(e.Algorithm) if algorithm == "" { return fmt.Errorf("%w: algorithm is required", ErrInvalidSecretEnvelope) } if algorithm != algorithmAES256GCM { return fmt.Errorf("%w: unsupported algorithm", ErrInvalidSecretEnvelope) } if strings.TrimSpace(e.KeyID) == "" { return fmt.Errorf("%w: key_id is required", ErrInvalidSecretEnvelope) } if e.KeyVersion == 0 { return fmt.Errorf("%w: key_version must be greater than 0", ErrInvalidSecretEnvelope) } if len(e.Nonce) < minNonceLength { return fmt.Errorf("%w: nonce is too short", ErrInvalidSecretEnvelope) } if len(e.Ciphertext) == 0 { return fmt.Errorf("%w: ciphertext is required", ErrInvalidSecretEnvelope) } return nil } // validateEnvelopeKey checks that the envelope is valid and its key ID and version are registered. func (s *Store) validateEnvelopeKey(ctx context.Context, env SecretEnvelope) error { if err := env.Validate(); err != nil { return err } registry := s.KeyRegistry() ok, err := registry.HasEnvelopeKey(ctx, env.KeyID, env.KeyVersion) if err != nil { if errors.Is(err, ErrEnvelopeKeyUnavailable) { return err } return fmt.Errorf("%w: %v", ErrEnvelopeKeyUnavailable, err) } if !ok { return fmt.Errorf("%w: key_id=%s version=%d", ErrUnknownEnvelopeKey, env.KeyID, env.KeyVersion) } return nil }