gastownhall/beads · error

ciphertext too short

Error message

ciphertext too short

What it means

decryptWithKey splits AES-GCM ciphertext into a nonce prefix and body, using gcm.NonceSize() (12 bytes) as the split point. This error is returned when the ciphertext is shorter than the nonce size, so no valid nonce+body structure exists. It means the stored value cannot possibly be a valid AES-GCM output from this library — it is truncated, empty-ish, or encrypted under some other scheme.

Source

Thrown at internal/storage/dolt/credentials.go:226

	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return nil, err
	}
	return gcm.Seal(nonce, nonce, []byte(plaintext), nil), nil
}

// decryptWithKey decrypts ciphertext using AES-GCM with the given key.
func decryptWithKey(encrypted []byte, key []byte) (string, error) {
	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(encrypted) < nonceSize {
		return "", fmt.Errorf("ciphertext too short")
	}
	nonce, ciphertext := encrypted[:nonceSize], encrypted[nonceSize:]
	plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		return "", err
	}
	return string(plaintext), nil
}

// encryptPassword encrypts a password using AES-GCM with the store's credential key.
func (s *DoltStore) encryptPassword(password string) ([]byte, error) {
	if password == "" {
		return nil, nil
	}
	s.mu.RLock()
	key := s.credentialKey
	s.mu.RUnlock()
	if key == nil {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Re-add the peer's credentials so the password is re-encrypted correctly (the stored value is unrecoverable if truncated)
  2. Check whether the row predates the AES-GCM scheme; if so, re-enter the password via the peer-add flow rather than trying to decrypt it
  3. Restore the affected federation_peers rows from a backup if corruption was accidental
  4. Ensure no tooling or scripts rewrite password_encrypted directly; only use bd's peer management commands
Defensive patterns

Strategy: type-guard

Validate before calling

func looksLikeAESGCM(blob []byte) bool { return len(blob) >= 12 } // nonce(12) + at least one ciphertext/tag byte

Type guard

func isDecryptableCiphertext(encrypted []byte) bool {
    const gcmNonceSize = 12
    return len(encrypted) >= gcmNonceSize
}

Try / catch

pwd, err := decryptPassword(encrypted)
if err != nil {
    if strings.Contains(err.Error(), "ciphertext too short") {
        // stored value is corrupt/foreign scheme — prompt to re-enter credentials
        return reAddPeerCredentials(peerName)
    }
    return err
}

Prevention

When it happens

Trigger: decryptWithKey is called with encrypted bytes of length < 12: a truncated or corrupted password_encrypted value in federation_peers, a value written by a different/older encryption scheme without a nonce prefix, manual tampering with the column, or a test passing deliberately short ciphertext.

Common situations: Database rows copied or migrated between workspaces losing bytes; manual SQL edits to password_encrypted; values produced by a pre-AES-GCM legacy format; restoring partial backups; test harnesses feeding malformed input.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/6f4d2d54a9de7481. Report an issue: GitHub.