regy 0.1.4

Private-by-default desktop agent for the Regy web interface
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>)")
    }
}