slackhq/nebula · error

invalid ciphertext blob - blob shorter than nonce length

Error message

invalid ciphertext blob - blob shorter than nonce length

What it means

splitNonceCiphertext requires the encrypted blob to be strictly longer than the nonce size, because valid AES-GCM output is nonce||ciphertext. A blob at or below nonceSize cannot contain any ciphertext, so aes256Decrypt rejects it as malformed rather than attempting to slice it.

Source

Thrown at cert/crypto.go:153

		return nil, fmt.Errorf("salt must be set in argon2Parameters")
	} else if len(params.salt) < 16 {
		return nil, fmt.Errorf("salt must be at least 128  bits")
	}

	key := argon2.IDKey(passphrase, params.salt, params.Iterations, params.Memory, params.Parallelism, keySize)

	return key, nil
}

// Prepends nonce to ciphertext
func joinNonceCiphertext(nonce []byte, ciphertext []byte) []byte {
	return append(nonce, ciphertext...)
}

// Splits nonce from ciphertext
func splitNonceCiphertext(blob []byte, nonceSize int) ([]byte, []byte, error) {
	if len(blob) <= nonceSize {
		return nil, nil, fmt.Errorf("invalid ciphertext blob - blob shorter than nonce length")
	}

	return blob[:nonceSize], blob[nonceSize:], nil
}

// EncryptAndMarshalSigningPrivateKey is a simple helper to encrypt and PEM encode a private key
func EncryptAndMarshalSigningPrivateKey(curve Curve, b []byte, passphrase []byte, kdfParams *Argon2Parameters) ([]byte, error) {
	ciphertext, err := aes256Encrypt(passphrase, kdfParams, b)
	if err != nil {
		return nil, err
	}

	b, err = proto.Marshal(&RawNebulaEncryptedData{
		EncryptionMetadata: &RawNebulaEncryptionMetadata{
			EncryptionAlgorithm: "AES-256-GCM",
			Argon2Parameters: &RawNebulaArgon2Parameters{
				Version:     kdfParams.version,
				Memory:      kdfParams.Memory,

View on GitHub (pinned to dd8f660c0a)

Solutions

  1. Verify the input is genuinely Nebula-encrypted data: decode the PEM block and confirm its Type matches an Encrypted*PrivateKeyBanner and its length exceeds the nonce size (12 bytes for GCM)
  2. Re-export/regenerate the encrypted key file - the existing blob is truncated or not encrypted data
  3. Confirm you are decrypting the right file and not a plaintext key or public certificate

Example fix

// before
block, _ := pem.Decode(data)
key, err := cert.DecryptAndUnmarshalSigningPrivateKey(pass, block.Bytes) // blob too short
// after
block, _ := pem.Decode(data)
if block == nil || len(block.Bytes) <= 12 {
    return fmt.Errorf("not encrypted nebula key data")
}
key, err := cert.DecryptAndUnmarshalSigningPrivateKey(pass, block.Bytes)
Defensive patterns

Strategy: validation

Validate before calling

if len(blob) <= 12 { return fmt.Errorf("blob too short to contain nonce+ciphertext: %d", len(blob)) }

Type guard

func isPlausibleEncryptedBlob(b []byte) bool { return len(b) > 12 }

Try / catch

key, err := cert.DecryptAndUnmarshalSigningPrivateKey(pass, blob)
if err != nil {
    if strings.Contains(err.Error(), "invalid ciphertext blob") { /* wrong/truncated input */ }
    return err
}

Prevention

When it happens

Trigger: Calling aes256Decrypt (via DecryptAndUnmarshalSigningPrivateKey) with a blob whose length <= nonceSize (12 bytes for AES-GCM): an empty blob, a bare key with no encryption wrapper, or corrupted/truncated PEM-decoded data.

Common situations: Attempting to decrypt a key file that was never encrypted; passing a PEM block that is actually a plaintext key or certificate; file truncation during transfer; decrypting with the wrong format/version of encrypted data.

Related errors


AI-assisted analysis of slackhq/nebula@dd8f660c0a (2026-09-03). Data as JSON: /api/errors/be318cb3b635c5fc. Report an issue: GitHub.