slackhq/nebula · critical

invalid passphrase or corrupt private key

Error message

invalid passphrase or corrupt private key

What it means

aes256Decrypt derives the key from the passphrase with Argon2id and opens the AES-256-GCM ciphertext; gcm.Open fails when authentication fails. The library collapses any authentication failure into this message, meaning the passphrase is wrong or the ciphertext/metadata was altered or truncated. It deliberately does not distinguish wrong passphrase from corruption.

Source

Thrown at cert/crypto.go:105

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

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

	nonce, ciphertext, err := splitNonceCiphertext(data, gcm.NonceSize())
	if err != nil {
		return nil, err
	}

	plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		return nil, fmt.Errorf("invalid passphrase or corrupt private key")
	}

	return plaintext, nil
}

func aes256DeriveKey(passphrase []byte, params *Argon2Parameters) ([]byte, error) {
	if params.salt == nil {
		params.salt = make([]byte, 32)
		if _, err := rand.Read(params.salt); err != nil {
			return nil, err
		}
	}

	// keySize of 32 bytes will result in AES-256 encryption
	key, err := deriveKey(passphrase, 32, params)
	if err != nil {
		return nil, err
	}

View on GitHub (pinned to dd8f660c0a)

Solutions

  1. Re-enter the exact passphrase used when the key was encrypted ( EncryptAndMarshalSigningPrivateKey ).
  2. Re-encrypt the private key with the new passphrase if the original is lost — the old ciphertext cannot be recovered.
  3. Verify the encrypted key file is byte-identical to the original (no edits, CRLF conversion, or truncation).
  4. Ensure the Argon2 parameters stored in the file metadata were not altered.

Example fix

// before
key, _, err := cert.DecryptAndUnmarshalSigningPrivateKey([]byte(os.Getenv("NEBULA_PASS")), pemBytes) // wrong env var value
// after
pass := []byte(os.Getenv("NEBULA_KEY_PASSWORD"))
if len(pass) == 0 {
    return fmt.Errorf("NEBULA_KEY_PASSWORD not set")
}
key, _, err := cert.DecryptAndUnmarshalSigningPrivateKey(pass, pemBytes)
Defensive patterns

Strategy: try-catch

Validate before calling

// cannot be validated beforehand; verify passphrase is present and non-empty
pass := []byte(os.Getenv("NEBULA_KEY_PASSWORD"))
if len(pass) == 0 {
    return fmt.Errorf("passphrase must be provided to decrypt host key")
}

Try / catch

key, groups, err := cert.DecryptAndUnmarshalSigningPrivateKey(pass, pemBytes)
if err != nil {
    if err.Error() == "invalid passphrase or corrupt private key" {
        return fmt.Errorf("wrong passphrase for encrypted host key (or file corrupted); re-check NEBULA_KEY_PASSWORD")
    }
    return err
}

Prevention

When it happens

Trigger: Calling DecryptAndUnmarshalSigningPrivateKey with a passphrase that differs from the one used at encryption, or with ciphertext bytes that were modified/truncated (splitNonceCiphertext passed but GCM tag mismatched).

Common situations: Typo or changed passphrase in config (key.password / encrypted_host_key), encrypted key file edited or copied incorrectly, different Argon2 parameters than used at encryption time, environment variable interpolation altering the passphrase.

Related errors


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