src.nth.io/

summaryrefslogtreecommitdiff
path: root/src/pairing.rs
blob: 715d7a873b7e71ced38106465fe7e0696fce9f90 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//! Manual pairing code decoding (Matter Core spec 5.1.4.1).
//!
//! An 11-digit (no VID/PID) or 21-digit (with VID/PID) decimal code packs a
//! 4-bit short discriminator and the 27-bit setup passcode, guarded by a
//! trailing Verhoeff check digit. QR payloads ("MT:...") are not accepted;
//! the primary ecosystem always shows the numeric code.

#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum PairingCodeError {
    #[error(
        "QR payloads are not supported; enter the numeric pairing code shown by the primary ecosystem"
    )]
    QrPayload,
    #[error("pairing code must be 11 or 21 digits (got {0})")]
    BadLength(usize),
    #[error("pairing code check digit is wrong; re-check the digits")]
    BadCheckDigit,
    #[error("pairing code decodes to an invalid setup passcode")]
    BadPasscode,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Onboarding {
    pub passcode: u32,
    /// Upper 4 bits of the 12-bit discriminator, as carried in manual codes.
    pub short_discriminator: u8,
}

/// Setup passcodes the spec forbids (trivial/sequential values).
const INVALID_PASSCODES: [u32; 12] = [
    0, 11111111, 22222222, 33333333, 44444444, 55555555, 66666666, 77777777, 88888888, 99999999,
    12345678, 87654321,
];

pub fn parse_manual_code(raw: &str) -> Result<Onboarding, PairingCodeError> {
    let trimmed = raw.trim();
    if trimmed.starts_with("MT:") {
        return Err(PairingCodeError::QrPayload);
    }
    let digits: Vec<u8> = trimmed
        .chars()
        .filter(|c| !c.is_whitespace() && *c != '-')
        .map(|c| {
            c.to_digit(10)
                .map(|d| d as u8)
                .ok_or(PairingCodeError::BadLength(0))
        })
        .collect::<Result<_, _>>()
        .map_err(|_| PairingCodeError::BadLength(trimmed.len()))?;

    if digits.len() != 11 && digits.len() != 21 {
        return Err(PairingCodeError::BadLength(digits.len()));
    }

    let payload: String = digits[..digits.len() - 1]
        .iter()
        .map(|d| (b'0' + d) as char)
        .collect();
    if verhoeff::calculate(payload.as_str()) != digits[digits.len() - 1] {
        return Err(PairingCodeError::BadCheckDigit);
    }

    let d1 = u32::from(digits[0]);
    let chunk2 = digits[1..6]
        .iter()
        .fold(0u32, |acc, &d| acc * 10 + u32::from(d));
    let chunk3 = digits[6..10]
        .iter()
        .fold(0u32, |acc, &d| acc * 10 + u32::from(d));

    // digit 1  = (VID_PID_PRESENT << 2) | (discriminator >> 10)
    // digits 2-6 = ((discriminator & 0x300) << 6) | (passcode & 0x3FFF)
    // digits 7-10 = passcode >> 14
    let short_discriminator = (((d1 & 0x3) << 2) | ((chunk2 >> 14) & 0x3)) as u8;
    let passcode = (chunk3 << 14) | (chunk2 & 0x3FFF);

    if passcode >= 1 << 27 || INVALID_PASSCODES.contains(&passcode) {
        return Err(PairingCodeError::BadPasscode);
    }
    Ok(Onboarding {
        passcode,
        short_discriminator,
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn decodes_the_canonical_test_code() {
        // The Matter SDK test onboarding payload: passcode 20202021,
        // discriminator 3840 (0xF00) => short discriminator 15, manual code
        // 34970112332.
        let onboarding = parse_manual_code("34970112332").unwrap();
        assert_eq!(
            onboarding,
            Onboarding {
                passcode: 20202021,
                short_discriminator: 15,
            }
        );
    }

    #[test]
    fn accepts_hyphenated_input() {
        assert!(parse_manual_code("3497-011-2332").is_ok());
    }

    #[test]
    fn rejects_qr_payloads_with_guidance() {
        assert_eq!(
            parse_manual_code("MT:Y.K90SO527JA0648G00"),
            Err(PairingCodeError::QrPayload)
        );
    }

    #[test]
    fn rejects_wrong_lengths_and_check_digits() {
        assert!(matches!(
            parse_manual_code("1234"),
            Err(PairingCodeError::BadLength(_))
        ));
        assert_eq!(
            parse_manual_code("34970112331"),
            Err(PairingCodeError::BadCheckDigit)
        );
    }
}