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)
);
}
}
|