use std::net::{IpAddr, SocketAddr};
use ring::hmac;
use crate::credential::wire::base64url_decode;
use crate::scope::fold_ip;
pub const NONCE_LEN: usize = 20;
pub const NONCE_BUCKET_SECS: u64 = 60;
const NONCE_DOMAIN_TAG: &[u8] = b"dig:stun:nonce:v1";
const NONCE_FAMILY_IPV4: u8 = 0x01;
const NONCE_FAMILY_IPV6: u8 = 0x02;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NonceCheck {
Fresh,
Stale,
Invalid,
}
pub struct NonceIssuer {
secret: [u8; 32],
}
impl NonceIssuer {
pub fn new_random() -> Self {
use ring::rand::{SecureRandom, SystemRandom};
let mut secret = [0u8; 32];
SystemRandom::new()
.fill(&mut secret)
.expect("OS CSPRNG must be available to generate a STUN nonce-issuer secret");
Self { secret }
}
pub fn from_secret(secret: [u8; 32]) -> Self {
Self { secret }
}
pub fn issue(&self, source: SocketAddr, now_unix_secs: u64) -> [u8; NONCE_LEN] {
let bucket = (now_unix_secs / NONCE_BUCKET_SECS) as u32;
let tag = self.tag_for(bucket, source);
let mut nonce = [0u8; NONCE_LEN];
nonce[..4].copy_from_slice(&bucket.to_be_bytes());
nonce[4..].copy_from_slice(&tag);
nonce
}
pub fn check(
&self,
nonce_attr_value: &[u8],
source: SocketAddr,
now_unix_secs: u64,
) -> NonceCheck {
let decoded = match base64url_decode(nonce_attr_value) {
Some(bytes) if bytes.len() == NONCE_LEN => bytes,
_ => return NonceCheck::Invalid,
};
let bucket = u32::from_be_bytes(decoded[..4].try_into().expect("4-byte slice"));
let carried_tag = &decoded[4..NONCE_LEN];
let expected_tag = self.tag_for(bucket, source);
if !constant_time_eq(carried_tag, &expected_tag) {
return NonceCheck::Invalid;
}
let now_bucket = (now_unix_secs / NONCE_BUCKET_SECS) as u32;
if bucket == now_bucket || bucket == now_bucket.wrapping_sub(1) {
NonceCheck::Fresh
} else {
NonceCheck::Stale
}
}
fn tag_for(&self, bucket: u32, source: SocketAddr) -> [u8; 16] {
let key = hmac::Key::new(hmac::HMAC_SHA256, &self.secret);
let mut message = Vec::with_capacity(NONCE_DOMAIN_TAG.len() + 4 + 1 + 16 + 2);
message.extend_from_slice(NONCE_DOMAIN_TAG);
message.extend_from_slice(&bucket.to_be_bytes());
match fold_ip(source.ip()) {
IpAddr::V4(v4) => {
message.push(NONCE_FAMILY_IPV4);
message.extend_from_slice(&v4.octets());
}
IpAddr::V6(v6) => {
message.push(NONCE_FAMILY_IPV6);
message.extend_from_slice(&v6.octets());
}
}
message.extend_from_slice(&source.port().to_be_bytes());
let full = hmac::sign(&key, &message);
let mut tag = [0u8; 16];
tag.copy_from_slice(&full.as_ref()[..16]);
tag
}
}
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
}