sipeed/picoclaw · error

decrypt media: %w

Error message

decrypt media: %w

What it means

Optional AES-256-CBC decryption of inbound WeCom media failed (media.go:305-313). The key comes from the message payload (GetAESKey, base64 -> 32 bytes; IV = first 16 bytes of the key; PKCS7 unpad). The %w wraps concrete causes from decryptAESCBC: empty ciphertext, ciphertext length not a multiple of the 16-byte block, or invalid PKCS7 padding - the classic signatures of a key mismatch or of feeding plaintext to the decrypter.

Source

Thrown at pkg/channels/wecom/media.go:312

		return "", fmt.Errorf("download media returned HTTP %d", resp.StatusCode)
	}

	data, err := io.ReadAll(io.LimitReader(resp.Body, wecomOutboundMediaMaxBytes+1))
	if err != nil {
		return "", fmt.Errorf("read media: %w", err)
	}
	if len(data) > wecomOutboundMediaMaxBytes {
		return "", fmt.Errorf("media too large")
	}

	if aesKey != "" {
		key, keyErr := decodeMediaAESKey(aesKey)
		if keyErr != nil {
			return "", keyErr
		}
		data, err = decryptAESCBC(key, data)
		if err != nil {
			return "", fmt.Errorf("decrypt media: %w", err)
		}
	}

	filename, contentType := detectWeComMediaMetadata(
		data,
		msgID+fallbackExt,
		resp.Header.Get("Content-Type"),
		resourceURL,
		resp.Header.Get("Content-Disposition"),
	)
	ext := filepath.Ext(filename)
	if ext == "" {
		ext = inferMediaExt(contentType, fallbackExt)
	}
	mediaDir := filepath.Join(os.TempDir(), "picoclaw_media")
	if mkdirErr := os.MkdirAll(mediaDir, 0o700); mkdirErr != nil {
		return "", fmt.Errorf("mkdir media dir: %w", mkdirErr)
	}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Confirm the aes_key being used is the one from the same inbound message payload (payload.GetAESKey()), not a static config key
  2. Check len(body) % 16 == 0 before decrypting; if not, the body is almost certainly plaintext - skip decryption or treat as protocol error
  3. Verify the key decodes to exactly 32 bytes: WeCom EncodingAESKey is a 43-char base64 string (decodeMediaAESKey appends '=' if needed)
  4. Capture one failing URL+aes_key pair and reproduce decryption offline against the raw bytes to see the specific wrapped error (padding vs block size)

Example fix

// before: decrypt whenever a key is present
if aesKey != "" {
    key, _ := decodeMediaAESKey(aesKey)
    data, err = decryptAESCBC(key, data)
    if err != nil {
        return "", fmt.Errorf("decrypt media: %w", err)
    }
}

// after: only attempt CBC when the body is block-aligned
if aesKey != "" && len(data)%aes.BlockSize == 0 && detectWeComFiletype(data) == ("", "") {
    key, keyErr := decodeMediaAESKey(aesKey)
    if keyErr != nil {
        return "", keyErr
    }
    data, err = decryptAESCBC(key, data)
    if err != nil {
        return "", fmt.Errorf("decrypt media: %w", err)
    }
}
Defensive patterns

Strategy: validation

Validate before calling

// prechecks that eliminate the common decrypt failures
func mediaDecryptable(aesKey string, body []byte) bool {
    if aesKey == "" {
        return true // nothing to decrypt
    }
    key, err := base64.StdEncoding.DecodeString(aesKey + "=")
    if err != nil || len(key) != 32 {
        return false // malformed key
    }
    return len(body) > 0 && len(body)%16 == 0 // block-aligned ciphertext
}

Prevention

When it happens

Trigger: decodeMediaAESKey succeeded but decryptAESCBC rejected the body: aes_key from the payload does not match the corp/app that encrypted the media; the CDN body was actually unencrypted (often non-block-aligned, e.g. an HTML error page or a plain JPEG) while aesKey was non-empty; or the download was truncated to a non-multiple of 16.

Common situations: EncodingAESKey rotated on the WeCom admin side while messages still reference the old key; test/prod apps mixed up; CDN returning a 200 error page that then fails padding checks; key copied with trailing characters so it decodes to a wrong 32 bytes.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/60c0aef4b34e400a. Report an issue: GitHub.