grafana/k6 · error

invalid symmetric JWK: %w

Error message

invalid symmetric JWK: %w

What it means

Umbrella error thrown by k6 WebCrypto when a symmetric JWK fails semantic validation after parsing. extractSymmetricJWK (internal/js/modules/k6/webcrypto/jwk.go:56-58) wraps symmetricJWK.validate's error with 'invalid symmetric JWK: %w'; the inner error is either 'invalid key type: <kty>' (kty is not 'oct') or 'key (k) is required' (the k field is missing/empty). The message therefore nests the real cause after the colon.

Source

Thrown at internal/js/modules/k6/webcrypto/jwk.go:57

		return fmt.Errorf("invalid key type: %s", jwk.Kty)
	}

	if jwk.K == "" {
		return errors.New("key (k) is required")
	}

	return nil
}

// extractSymmetricJWK extracts the symmetric key from a given JWK key (JSON data).
func extractSymmetricJWK(jsonKeyData []byte) ([]byte, error) {
	sk := symmetricJWK{}
	if err := json.Unmarshal(jsonKeyData, &sk); err != nil {
		return nil, fmt.Errorf("failed to parse symmetric JWK: %w", err)
	}

	if err := sk.validate(); err != nil {
		return nil, fmt.Errorf("invalid symmetric JWK: %w", err)
	}

	skBytes, err := base64URLDecode(sk.K)
	if err != nil {
		return nil, fmt.Errorf("failed to decode symmetric key: %w", err)
	}

	return skBytes, nil
}

// exportSymmetricJWK exports a symmetric key as a map of JWK key parameters.
func exportSymmetricJWK(key *CryptoKey) (*JsonWebKey, error) {
	rawKey, ok := key.handle.([]byte)
	if !ok {
		return nil, errors.New("key's handle isn't a byte slice")
	}

	// wrap result into the object that is expected to be returned

View on GitHub (pinned to 93accf6570)

Solutions

  1. Read the nested cause: if it says 'key (k) is required', add the base64url-encoded secret as k.
  2. If it says 'invalid key type', see that kty is exactly 'oct' (lowercase).
  3. Use correct JSON field names: kty, k — both lowercase.

Example fix

// before
await crypto.subtle.importKey('jwk', { kty: 'oct' }, { name: 'AES-GCM' }, false, ['encrypt']);

// after
await crypto.subtle.importKey('jwk', { kty: 'oct', k: base64urlSecret }, { name: 'AES-GCM' }, false, ['encrypt']);
Defensive patterns

Strategy: validation

Validate before calling

function assertValidOctJwk(jwk) {
  if (jwk.kty !== 'oct') throw new Error(`expected kty 'oct', got '${jwk.kty}'`);
  if (!jwk.k || typeof jwk.k !== 'string') throw new Error('oct JWK requires a non-empty k field');
}

Type guard

const isValidOctJwk = (jwk) => jwk != null && jwk.kty === 'oct' && typeof jwk.k === 'string' && jwk.k.length > 0;

Prevention

When it happens

Trigger: importKey('jwk', { kty: 'oct' }, { name: 'AES-GCM' }) — no k field; importKey('jwk', { k: '...', kty: 'RSA' }, HMAC) — wrong kty; a JWK where the k field name is capitalized or misspelled ('K') so it unmarshals as empty.

Common situations: Minimal hand-written JWK fixtures missing the secret; field-name casing errors from manual transcription; a key-exchange endpoint returning metadata-only JWKs without secret material.

Related errors


AI-assisted analysis of grafana/k6@93accf6570 (2026-08-15). Data as JSON: /api/errors/db5a753178664b17. Report an issue: GitHub.