use secrecy::{ExposeSecret, SecretString};
use crate::internal_auth::{InternalAuthNError, InternalAuthenticator, PlatformIdentity};
#[derive(Clone)]
pub struct SharedSecretInternalAuthenticator {
secret: SecretString,
peer_name: String,
}
impl std::fmt::Debug for SharedSecretInternalAuthenticator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SharedSecretInternalAuthenticator")
.field("peer_name", &self.peer_name)
.finish_non_exhaustive()
}
}
pub const REDACTED_PLACEHOLDER: &str = "<redacted>";
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum InvalidSharedSecret {
#[error("shared secret must not be empty")]
Empty,
#[error(
"shared secret is the literal `{REDACTED_PLACEHOLDER}` placeholder, which a serialized \
config writes in place of the real secret"
)]
RedactedPlaceholder,
}
impl SharedSecretInternalAuthenticator {
pub fn try_new(secret: SecretString, peer_name: String) -> Result<Self, InvalidSharedSecret> {
match secret.expose_secret() {
"" => Err(InvalidSharedSecret::Empty),
REDACTED_PLACEHOLDER => Err(InvalidSharedSecret::RedactedPlaceholder),
_ => Ok(Self { secret, peer_name }),
}
}
}
impl InternalAuthenticator for SharedSecretInternalAuthenticator {
async fn authenticate(&self, token: &str) -> Result<PlatformIdentity, InternalAuthNError> {
if constant_time_eq(token.as_bytes(), self.secret.expose_secret().as_bytes()) {
Ok(PlatformIdentity::Shared {
name: self.peer_name.clone(),
})
} else {
tracing::warn!(
peer_name = %self.peer_name,
"platform-plane authentication rejected: shared secret did not match"
);
Err(InternalAuthNError::InvalidToken)
}
}
}
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
#[test]
fn an_empty_secret_is_refused() {
assert_eq!(
SharedSecretInternalAuthenticator::try_new(SecretString::from(""), "peer".to_owned())
.unwrap_err(),
InvalidSharedSecret::Empty
);
}
#[test]
fn the_redaction_placeholder_is_refused() {
assert_eq!(
SharedSecretInternalAuthenticator::try_new(
SecretString::from(REDACTED_PLACEHOLDER),
"peer".to_owned()
)
.unwrap_err(),
InvalidSharedSecret::RedactedPlaceholder
);
}
#[tokio::test]
async fn an_empty_token_is_rejected_by_a_real_secret() {
let rejected = auth().authenticate("").await;
assert!(matches!(rejected, Err(InternalAuthNError::InvalidToken)));
}
fn auth() -> SharedSecretInternalAuthenticator {
SharedSecretInternalAuthenticator::try_new(SecretString::from("s3cr3t"), "peer".to_owned())
.expect("a non-empty secret")
}
#[tokio::test]
async fn accepts_matching_secret_and_resolves_shared_identity() {
let identity = auth().authenticate("s3cr3t").await.expect("valid secret");
assert_eq!(
identity,
PlatformIdentity::Shared {
name: "peer".to_owned()
}
);
assert_eq!(identity.peer_name(), "peer");
}
#[tokio::test]
async fn rejects_wrong_secret() {
let err = auth().authenticate("nope").await.unwrap_err();
assert!(matches!(err, InternalAuthNError::InvalidToken));
}
#[tokio::test]
async fn rejects_empty_and_prefix_tokens() {
assert!(auth().authenticate("").await.is_err());
assert!(auth().authenticate("s3cr3").await.is_err());
assert!(auth().authenticate("s3cr3tt").await.is_err());
}
#[test]
fn constant_time_eq_matches_std_eq() {
assert!(constant_time_eq(b"abc", b"abc"));
assert!(!constant_time_eq(b"abc", b"abd"));
assert!(!constant_time_eq(b"abc", b"ab"));
assert!(constant_time_eq(b"", b""));
}
#[test]
fn debug_never_leaks_secret() {
let rendered = format!("{:?}", auth());
assert!(rendered.contains("peer"));
assert!(!rendered.contains("s3cr3t"));
}
}