rspotd/
lib.rs

1use block_modes::{BlockMode, Cbc};
2use block_padding::ZeroPadding;
3use des::Des;
4use regex::Regex;
5use std::collections::BTreeMap;
6use std::error::Error;
7use std::mem::replace;
8use time::macros::format_description;
9use time::Date;
10use time::Duration;
11
12// Create a date range using a start and end date
13struct DateRange(Date, Date);
14impl Iterator for DateRange {
15    type Item = Date;
16    fn next(&mut self) -> Option<Self::Item> {
17        if self.0 <= self.1 {
18            let next = self.0 + Duration::days(1);
19            Some(replace(&mut self.0, next))
20        } else {
21            None
22        }
23    }
24}
25
26fn derive_from_input(date: &str, padded_seed: &str) -> String {
27    use vals::{ALPHANUM, TABLE1, TABLE2};
28    let fmt_date = validate_date(date).unwrap();
29    // Split date in YYYY-MM-DD format by hypen into a Vector of strings
30    let date_components: Vec<i32> = date
31        .split('-')
32        .map(|i| i.parse::<i32>().expect("Error parsing date string"))
33        .collect();
34    let year = date_components[0];
35    // Convert year to a string to get last two chars, then cast to i32
36    let year_trimmed = year.to_string()[2..].parse::<i32>().unwrap();
37    let month = date_components[1];
38    let day = date_components[2];
39    let day_of_week = fmt_date.weekday().number_days_from_monday() as usize;
40    let a: Vec<i32> = (0..8)
41        .map(|i| match i {
42            0..=4 => TABLE1[day_of_week][i],
43            5 => day,
44            6 => {
45                if ((year_trimmed + month) - day) < 0 {
46                    (((year_trimmed + month) - day) + 36) % 36
47                } else {
48                    ((year_trimmed + month) - day) % 36
49                }
50            }
51            _ => (((3 + ((year_trimmed + month) % 12)) * day) % 37) % 36,
52        })
53        .collect();
54    let b: Vec<i32> = padded_seed.chars().map(|c| c as i32).collect();
55    let first_eight: Vec<i32> = (0..8).map(|i| (a[i] + b[i]) % 36).collect();
56    let sum_of_parts: i32 = first_eight.iter().sum();
57    let mut c: Vec<i32> = Vec::from(first_eight);
58    c.push(sum_of_parts % 36);
59    let last_value = (c[8] % 6).pow(2) as f64;
60    if (last_value - last_value.floor()) < 0.5 {
61        c.push(last_value.floor() as i32)
62    } else {
63        c.push(last_value.ceil() as i32);
64    }
65    let d: Vec<i32> = (0..10)
66        .map(|i| c[TABLE2[(c[8] % 6) as usize][i] as usize])
67        .collect();
68    let vec_a: Vec<i32> = padded_seed
69        .chars()
70        .enumerate()
71        .map(|(i, c)| (c as i32 + d[i]) % 36)
72        .collect();
73    let vec_b: String = (0..10)
74        .map(|i: i32| ALPHANUM[vec_a[i as usize] as usize])
75        .collect();
76    return vec_b;
77}
78
79fn pad_seed(seed: &str) -> String {
80    let mut padded = seed.to_string();
81    if seed.len() == 4 {
82        let diff = format!("{}{}", &seed, &seed[0..2]);
83        padded.push_str(&diff);
84        return padded;
85    }
86    let diff: String = (0..10 - seed.len())
87        .into_iter()
88        .map(|i| seed.as_bytes()[i as usize] as char)
89        .into_iter()
90        .collect();
91    padded.push_str(&diff);
92    padded
93}
94
95fn validate_seed(seed: &str) -> Result<String, Box<dyn Error>> {
96    use vals::DEFAULT_SEED;
97    if seed == DEFAULT_SEED {
98        return Ok(seed.to_string());
99    }
100    // seed must be 4-8 characters
101    if seed.len() < 4 || seed.len() > 8 {
102        Err("Seed should be >= 4 and <= 8 characters long.")?;
103    }
104    let padded_seed: String = pad_seed(seed);
105    return Ok(padded_seed);
106}
107
108fn validate_date(date: &str) -> Result<Date, Box<dyn Error>> {
109    let fmt = format_description!("[year]-[month]-[day]");
110    let date_regex: Regex = Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap();
111    if !date_regex.is_match(date) {
112        Err("Invalid date format, must be YYYY-MM-DD")?;
113    }
114    let parsed_date = Date::parse(date, fmt);
115    if parsed_date.is_ok() {
116        return Ok(parsed_date.unwrap());
117    } else {
118        let err = format!(
119            "Unable to parse date '{}'. Year, month or day value out of range.",
120            date
121        );
122        return Err(err.into());
123    }
124}
125
126fn validate_range(date_begin: &str, date_end: &str) -> Result<bool, Box<dyn Error>> {
127    let maybe_begin = validate_date(date_begin);
128    let maybe_end = validate_date(date_end);
129    if maybe_begin.is_err() {
130        return Err(maybe_begin.unwrap_err());
131    } else if maybe_end.is_err() {
132        return Err(maybe_end.unwrap_err());
133    }
134    let begin = maybe_begin.unwrap();
135    let end = maybe_end.unwrap();
136    if end - begin <= time::Duration::days(0) {
137        Err("Invalid date range. Beginning date must occur before end date, and the values cannot be the same.")?;
138    }
139    if end - begin > Duration::days(365) {
140        Err("Invalid date range. Official tooling does not allow a date range exceeding 1 year.")?;
141    }
142    return Ok(true);
143}
144
145/// Generate an ARRIS/Commscope modem password given a date and seed
146///
147/// # Examples
148///
149/// ## Using default seed
150///
151/// ```no_run
152/// use rspotd::{generate, vals::DEFAULT_SEED};
153///
154/// generate("2021-12-25", DEFAULT_SEED).unwrap();
155/// ```
156///
157/// ## Using custom seed
158///
159/// ```no_run
160/// use rspotd::generate;
161///
162/// generate("2021-12-25", "ABCDEFGH").unwrap();
163/// ```
164pub fn generate(date: &str, seed: &str) -> Result<String, Box<dyn Error>> {
165    let valid_date = validate_date(date);
166    if valid_date.is_err() {
167        Err(valid_date.unwrap_err())?;
168    }
169    let valid_seed = validate_seed(seed);
170    if valid_seed.is_err() {
171        let err_str = &valid_seed.as_ref();
172        Err(err_str.unwrap_err().to_string())?;
173    }
174
175    return Ok(derive_from_input(date, &valid_seed.unwrap()));
176}
177
178/// Generate a series of ARRIS/Commscope modem passwords given a start and end date and a seed
179///
180/// # Examples
181///
182/// ## Using default seed
183///
184/// ```no_run
185/// use rspotd::{generate_multiple, vals::DEFAULT_SEED};
186///
187/// generate_multiple("2021-07-23", "2022-07-28", DEFAULT_SEED).unwrap();
188/// ```
189///
190/// ## Using custom seed
191///
192/// ```no_run
193/// use rspotd::generate_multiple;
194///
195/// generate_multiple("2021-07-23", "2022-07-28", "ABCDABCD").unwrap();
196/// ```
197pub fn generate_multiple(
198    date_begin: &str,
199    date_end: &str,
200    seed: &str,
201) -> Result<BTreeMap<String, String>, Box<dyn Error>> {
202    let begin = validate_date(date_begin);
203    let end = validate_date(date_end);
204    if begin.is_err() {
205        return Err(begin.unwrap_err());
206    }
207    if end.is_err() {
208        return Err(end.unwrap_err());
209    }
210    let valid_range = validate_range(date_begin, date_end);
211    if valid_range.is_err() {
212        return Err(valid_range.unwrap_err());
213    }
214    let date_range = DateRange(begin.unwrap(), end.unwrap());
215    let valid_seed = validate_seed(seed);
216    if valid_seed.is_err() {
217        return Err(valid_seed.unwrap_err());
218    }
219    let mut potd_map = BTreeMap::new();
220    for date in date_range {
221        let date_string = date.to_string();
222        let potd = derive_from_input(&date_string, valid_seed.as_ref().unwrap());
223        potd_map.insert(date_string, potd);
224    }
225    return Ok(potd_map);
226}
227
228/// Creates the required dot-delimited hex string correlating to the provided seed DES-encrypted.
229///
230/// The value provided by this function can be added to a modem configuration file, and a modem with this config
231/// will subsequently respond to a password of the day generated by the same seed used to create the DES-encrypted value.
232///
233/// Note, you cannot configure your modem from the subscriber-side. Your modem downloads its configuration
234/// typically via TFTP from a server inside your ISP's infrastructure.
235///
236/// The default ARRIS/CommScope value is provided if you use the default seed. I have not yet figured out
237/// how they generate the DES-encrypted value with a seed that exceeds a block size of 8, so I have to hardcode the value.
238///
239/// In the official tooling, the DES value is not provided if you select the "Use default seed" checkbox, and
240/// as far as I can tell, the software maintains a firm understanding throughout that your seed is between
241/// 4 and 8 characters.
242///
243/// Only one such value will exist for any number of passwords of a given seed; the modem infers the seed from this value.
244///
245/// ## Example
246///
247/// ```no_run
248/// use rspotd::seed_to_des;
249///
250/// seed_to_des("ASDF").unwrap();
251/// ```
252pub fn seed_to_des(seed: &str) -> Result<String, Box<dyn Error>> {
253    use vals::{DEFAULT_DES, DEFAULT_SEED};
254    if seed == DEFAULT_SEED {
255        return Ok(DEFAULT_DES.to_string());
256    }
257    if seed.len() < 4 || seed.len() > 8 {
258        Err("Seed should be >= 4 and <= 8 characters long.")?;
259    }
260    let key = [20, 157, 64, 213, 193, 46, 85, 2];
261    let iv = [0, 0, 0, 0, 0, 0, 0, 0];
262    type DesCbc = Cbc<Des, ZeroPadding>;
263    let cipher = DesCbc::new_from_slices(&key, &iv).unwrap();
264    let mut seed_buffer = [0u8; 8];
265    seed_buffer[..seed.len()].copy_from_slice(seed.as_bytes());
266    let encrypted_seed = cipher.encrypt(&mut seed_buffer, seed.len()).unwrap();
267    let seed_string: String = encrypted_seed
268        .iter()
269        .map(|i| {
270            if i == &encrypted_seed[7] {
271                format!("{:X}", i)
272            } else {
273                format!("{:X}.", i)
274            }
275        })
276        .collect();
277    Ok(seed_string)
278}
279
280#[cfg(test)]
281mod tests;
282pub mod vals;