ekzhang/bore · error

invalid secret

Error message

invalid secret

What it means

In `Authenticator::server_handshake`, the client replied to the challenge with an `Authenticate(tag)` whose HMAC did not match the challenge under the server's secret (`validate` returned false). The server rejects the client as knowing the wrong secret.

Solutions

  1. Re-enter the exact server secret on the client (watch for quotes, spaces, newlines): `bore local <port> --to <host> --secret <exact-secret>`.
  2. Compare secrets on both sides (e.g. hash them) to confirm they match.
  3. If the secret was rotated, update all clients with the new secret.
  4. Fix the env var / config file supplying the secret if it's malformed.

Example fix

// before (trailing whitespace from shell)
bore local 3000 --to bore.example.com --secret 'mysecret '
// after
bore local 3000 --to bore.example.com --secret mysecret
Defensive patterns

Strategy: validation

Validate before calling

let secret = std::env::var("BORE_SECRET").unwrap_or_default();
if secret != secret.trim() || secret.is_empty() {
    bail!("BORE_SECRET is empty or contains stray whitespace");
}

Try / catch

match Client::new(...).await {
    Err(e) if e.to_string().contains("invalid secret") => {
        eprintln!("secret mismatch: compare BORE_SECRET with the server's --secret");
        std::process::exit(3);
    }
    other => other,
}

Prevention

When it happens

Trigger: Client configured with a different secret string than the server's `--secret`; garbled or whitespace-damaged secret in env/config; a stale client cached from before a server secret rotation.

Common situations: Secret typo or copy-paste including quotes/trailing whitespace; secret rotated on the server but not the client (or vice versa); secrets sourced from different env vars per environment (staging vs production).

Related errors


AI-assisted analysis of ekzhang/bore@00a735a899 (2026-09-08). Data as JSON: /api/errors/f9a8ff7d49fa6c3a. Report an issue: GitHub.

Appendix: source

Thrown at src/auth.rs:59

        if let Ok(tag) = hex::decode(tag) {
            let mut hmac = self.0.clone();
            hmac.update(challenge.as_bytes());
            hmac.verify_slice(&tag).is_ok()
        } else {
            false
        }
    }

    /// As the server, send a challenge to the client and validate their response.
    pub async fn server_handshake<T: AsyncRead + AsyncWrite + Unpin>(
        &self,
        stream: &mut Delimited<T>,
    ) -> Result<()> {
        let challenge = Uuid::new_v4();
        stream.send(ServerMessage::Challenge(challenge)).await?;
        match stream.recv_timeout().await? {
            Some(ClientMessage::Authenticate(tag)) => {
                ensure!(self.validate(&challenge, &tag), "invalid secret");
                Ok(())
            }
            _ => bail!("server requires secret, but no secret was provided"),
        }
    }

    /// As the client, answer a challenge to attempt to authenticate with the server.
    pub async fn client_handshake<T: AsyncRead + AsyncWrite + Unpin>(
        &self,
        stream: &mut Delimited<T>,
    ) -> Result<()> {
        let challenge = match stream.recv_timeout().await? {
            Some(ServerMessage::Challenge(challenge)) => challenge,
            _ => bail!("expected authentication challenge, but no secret was required"),
        };
        let tag = self.answer(&challenge);
        stream.send(ClientMessage::Authenticate(tag)).await?;
        Ok(())

View on GitHub (pinned to 00a735a899)