use std::fmt;
use data_encoding::BASE64URL_NOPAD;
use ring::rand::{SecureRandom, SystemRandom};
use super::SecretBytes;
const PAIRING_ALPHABET: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
const PAIRING_CODE_LENGTH: usize = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SecretGenerationError;
pub(crate) trait RandomSource: Send + Sync {
fn fill(&self, destination: &mut [u8]) -> Result<(), SecretGenerationError>;
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct SystemRandomSource;
impl RandomSource for SystemRandomSource {
fn fill(&self, destination: &mut [u8]) -> Result<(), SecretGenerationError> {
SystemRandom::new()
.fill(destination)
.map_err(|_| SecretGenerationError)
}
}
#[derive(PartialEq, Eq)]
pub(crate) struct PairingCode(String);
impl PairingCode {
pub(crate) fn generate(random: &impl RandomSource) -> Result<Self, SecretGenerationError> {
let mut bytes = [0_u8; 8];
random.fill(&mut bytes)?;
Ok(Self::from_random_bytes(bytes))
}
pub(crate) fn from_random_bytes(mut bytes: [u8; 8]) -> Self {
bytes[0] &= 0x0f;
let value = u64::from_be_bytes(bytes);
let mut encoded = String::with_capacity(PAIRING_CODE_LENGTH);
for shift in (0..60).step_by(5).rev() {
encoded.push(PAIRING_ALPHABET[((value >> shift) & 0x1f) as usize] as char);
}
Self(encoded)
}
pub(crate) fn normalize(entered: &str) -> Option<String> {
if !entered.is_ascii() {
return None;
}
let mut normalized = String::with_capacity(PAIRING_CODE_LENGTH);
for byte in entered.bytes() {
let byte = match byte {
b' ' | b'-' => continue,
b'a'..=b'z' => byte.to_ascii_uppercase(),
other => other,
};
let byte = match byte {
b'O' => b'0',
b'I' | b'L' => b'1',
other => other,
};
if !PAIRING_ALPHABET.contains(&byte) {
return None;
}
normalized.push(byte as char);
}
(normalized.len() == PAIRING_CODE_LENGTH).then_some(normalized)
}
pub(crate) fn expose_for_display(&self) -> &str {
&self.0
}
}
impl fmt::Debug for PairingCode {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("PairingCode(<redacted>)")
}
}
#[derive(PartialEq, Eq)]
pub(crate) struct ClientCredential(SecretBytes<32>);
impl ClientCredential {
pub(crate) fn generate(random: &impl RandomSource) -> Result<Self, SecretGenerationError> {
let mut bytes = [0_u8; 32];
random.fill(&mut bytes)?;
Ok(Self::from_bytes(bytes))
}
pub(crate) fn from_bytes(bytes: [u8; 32]) -> Self {
Self(SecretBytes::new(bytes))
}
pub(crate) fn from_wire(encoded: &str) -> Option<Self> {
if encoded.len() != 43 {
return None;
}
let decoded = BASE64URL_NOPAD.decode(encoded.as_bytes()).ok()?;
let bytes: [u8; 32] = decoded.try_into().ok()?;
(BASE64URL_NOPAD.encode(&bytes) == encoded).then(|| Self::from_bytes(bytes))
}
pub(crate) fn expose_for_wire(&self) -> String {
BASE64URL_NOPAD.encode(self.0.expose())
}
pub(crate) fn as_secret(&self) -> &SecretBytes<32> {
&self.0
}
}
impl fmt::Debug for ClientCredential {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("ClientCredential(<redacted>)")
}
}