fish2018/pansou · error

ciphertext too short

Error message

ciphertext too short

What it means

decryptPassword decrypts AES-256-GCM ciphertext produced by encryptPassword. The ciphertext is expected to be base64(nonce || sealed). After base64 decoding, if the byte slice is shorter than the GCM nonce size (12 bytes), there is no room for a nonce, so the function refuses with 'ciphertext too short' instead of slicing out of bounds. It signals that the stored string was never produced by encryptPassword (or was truncated/corrupted).

Solutions

  1. Verify the stored value is a non-truncated base64 blob at least 16 bytes long when decoded (12-byte nonce + tag); if not, the plaintext is unrecoverable — reset the password by re-running encryptPassword with the known plaintext.
  2. Check whether the value predates the encryption scheme (plaintext); migrate by re-encrypting: enc, _ := p.encryptPassword(storedPlaintext).
  3. Confirm decryptPassword and encryptPassword use the same key and the same base64 encoding (StdEncoding vs URLEncoding mismatch corrupts decode).
  4. Add a length check before calling decryptPassword to surface a clearer error.

Example fix

// before
password, err := p.decryptPassword(account.Password) // 'ciphertext too short' on plaintext value
// after
if dec, err := base64.StdEncoding.DecodeString(account.Password); err != nil || len(dec) < 12 {
    // legacy/plaintext value: re-encrypt instead of decrypting
    enc, encErr := p.encryptPassword(account.Password)
    if encErr == nil { account.Password = enc }
}
password, err := p.decryptPassword(account.Password)
Defensive patterns

Strategy: validation

Validate before calling

func isDecryptable(s string) bool {
    raw, err := base64.StdEncoding.DecodeString(s)
    return err == nil && len(raw) >= 12 // nonceSize for AES-GCM
}
// call before p.decryptPassword; if false, treat as legacy plaintext and re-encrypt

Try / catch

password, err := p.decryptPassword(encrypted)
if err != nil {
    if err.Error() == "ciphertext too short" {
        // legacy plaintext value: re-encrypt in place
        if enc, encErr := p.encryptPassword(encrypted); encErr == nil {
            saveEncrypted(enc); return encrypted, nil
        }
    }
    return "", err
}

Prevention

When it happens

Trigger: decryptPassword is called with a stored password string that base64-decodes to fewer than 12 bytes — e.g. an empty string, a plaintext password stored instead of the encrypted form, a truncated DB field, or ciphertext corrupted/trimmed by storage.

Common situations: Legacy accounts whose passwords were stored in plaintext before encryption was introduced; manual DB edits; a migration that re-encoded the base64; copy-paste dropping characters from the stored value.

Related errors


AI-assisted analysis of fish2018/pansou@beaa561337 (2026-09-07). Data as JSON: /api/errors/389377c2e1ae7d22. Report an issue: GitHub.

Appendix: source

Thrown at plugin/gying/gying.go:1454

	// base64解码
	ciphertext, err := base64.StdEncoding.DecodeString(encrypted)
	if err != nil {
		return "", err
	}

	block, err := aes.NewCipher(key)
	if err != nil {
		return "", err
	}

	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}

	nonceSize := gcm.NonceSize()
	if len(ciphertext) < nonceSize {
		return "", fmt.Errorf("ciphertext too short")
	}

	nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
	plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		return "", err
	}

	return string(plaintext), nil
}

// ============ Cookie管理 ============

// ============ Cookie 与反爬处理 ============

// getScraperClient 通过反射拿到 cloudscraper 内部的 http.Client,
// 便于读取和回写 cookie jar。
func getScraperClient(scraper *cloudscraper.Scraper) (*http.Client, error) {

View on GitHub (pinned to beaa561337)