1use embedded_storage::nor_flash::{NorFlash, ReadNorFlash};
9
10use ssh_key::sha2::Digest;
11
12use log::{debug, error};
13
14use sunset::error::Error as SunsetError;
15
16use crate::config::{SSHStampConfig, UartPins};
17
18use sunset::sshwire::{self, OwnOrBorrow};
19use sunset_sshwire_derive::{SSHDecode, SSHEncode};
20
21pub const CONFIG_VERSION_SIZE: usize = 4;
23pub const CONFIG_HASH_SIZE: usize = 32;
24pub const CONFIG_AREA_SIZE: usize = 4096;
25pub const CONFIG_OFFSET: usize = 0x9000;
26
27#[derive(SSHEncode, SSHDecode)]
29struct FlashConfig<'a> {
30 version: u8,
31 config: OwnOrBorrow<'a, SSHStampConfig>,
32 hash: [u8; 32],
34}
35
36impl FlashConfig<'_> {
37 const BUF_SIZE: usize = 460; }
39
40fn config_hash(config: &SSHStampConfig) -> Result<[u8; 32], SunsetError> {
41 let mut h = ssh_key::sha2::Sha256::new();
42 sshwire::hash_ser(&mut h, config)?;
43 Ok(h.finalize().into())
44}
45
46pub fn load_or_create<F>(
57 flash: &mut F,
58 buf: &mut [u8],
59 default_mac: [u8; 6],
60 default_uart_pins: UartPins,
61) -> Result<SSHStampConfig, SunsetError>
62where
63 F: NorFlash,
64{
65 match load_checked(flash, buf) {
66 Ok(mut c) => {
67 debug!("Good existing config");
68 if c.wifi_ap_ssid.as_str() == "ssh-stamp" {
69 debug!("Migrating insecure default Access Point SSID, regenerating randomly");
70 c.wifi_ap_ssid = SSHStampConfig::generate_wifi_ssid()?;
71 if c.wifi_ap_pw.is_empty() {
72 c.wifi_ap_pw = SSHStampConfig::generate_wifi_password()?;
73 }
74 save(flash, buf, &c)?;
75 }
76 Ok(c)
77 }
78 Err(LoadError::Invalid(e)) => {
83 error!("Existing config present but invalid; refusing to overwrite it: {e}");
84 Err(e)
85 }
86 Err(LoadError::Absent) => {
89 debug!("No existing config found, creating a new one");
90 create(flash, buf, default_mac, default_uart_pins)
91 }
92 }
93}
94
95pub fn create<F>(
100 flash: &mut F,
101 buf: &mut [u8],
102 default_mac: [u8; 6],
103 default_uart_pins: UartPins,
104) -> Result<SSHStampConfig, SunsetError>
105where
106 F: NorFlash,
107{
108 let c = SSHStampConfig::new(default_mac, default_uart_pins)?;
109 save(flash, buf, &c)?;
110 debug!("Created new config");
112
113 Ok(c)
114}
115
116enum LoadError {
121 Absent,
124 Invalid(SunsetError),
127}
128
129fn load_checked<F>(flash: &mut F, buf: &mut [u8]) -> Result<SSHStampConfig, LoadError>
133where
134 F: ReadNorFlash,
135{
136 let Ok(offset) = u32::try_from(CONFIG_OFFSET) else {
139 return Err(LoadError::Invalid(SunsetError::msg(
140 "CONFIG_OFFSET overflow",
141 )));
142 };
143
144 if flash.read(offset, buf).is_err() {
145 error!("flash read error 0x{CONFIG_OFFSET:x}");
146 return Err(LoadError::Invalid(SunsetError::msg("flash error")));
148 }
149
150 let Ok((flash_config, _used)) = sshwire::read_ssh::<FlashConfig>(buf, None) else {
153 return Err(LoadError::Absent);
154 };
155
156 if flash_config.version != SSHStampConfig::CURRENT_VERSION {
157 error!("wrong config version on decode: {}", flash_config.version);
158 return Err(LoadError::Invalid(SunsetError::msg("wrong config version")));
159 }
160
161 let OwnOrBorrow::Own(config) = flash_config.config else {
163 return Err(LoadError::Invalid(SunsetError::msg(
164 "unexpected borrowed config",
165 )));
166 };
167
168 let calc_hash = config_hash(&config).map_err(LoadError::Invalid)?;
169
170 if calc_hash != flash_config.hash {
171 return Err(LoadError::Invalid(SunsetError::msg("bad config hash")));
172 }
173
174 Ok(config)
175}
176
177pub fn load<F>(flash: &mut F, buf: &mut [u8]) -> Result<SSHStampConfig, SunsetError>
183where
184 F: ReadNorFlash,
185{
186 match load_checked(flash, buf) {
187 Ok(c) => Ok(c),
188 Err(LoadError::Absent) => Err(SunsetError::msg("failed to decode flash config")),
189 Err(LoadError::Invalid(e)) => Err(e),
190 }
191}
192
193pub fn save<F>(flash: &mut F, buf: &mut [u8], config: &SSHStampConfig) -> Result<(), SunsetError>
198where
199 F: NorFlash,
200{
201 let sc = FlashConfig {
202 version: SSHStampConfig::CURRENT_VERSION,
203 config: OwnOrBorrow::Borrow(config),
204 hash: config_hash(config)?,
205 };
206
207 let l = sshwire::write_ssh(buf, &sc)?;
210
211 debug!("Erasing flash");
212
213 const { assert!(CONFIG_AREA_SIZE > FlashConfig::BUF_SIZE) };
214
215 let write_len = l
221 .checked_next_multiple_of(F::WRITE_SIZE)
222 .filter(|n| *n <= buf.len() && *n <= CONFIG_AREA_SIZE)
223 .ok_or_else(|| SunsetError::msg("encoded config too large for flash area"))?;
224
225 let offset =
226 u32::try_from(CONFIG_OFFSET).map_err(|_| SunsetError::msg("CONFIG_OFFSET overflow"))?;
227 let area_size = u32::try_from(CONFIG_AREA_SIZE)
228 .map_err(|_| SunsetError::msg("CONFIG_AREA_SIZE overflow"))?;
229
230 flash.erase(offset, offset + area_size).map_err(|_e| {
231 error!("flash erase error");
232 SunsetError::msg("flash erase error")
233 })?;
234
235 flash.write(offset, &buf[..write_len]).map_err(|_e| {
236 error!("flash write error");
237 SunsetError::msg("flash write error")
238 })?;
239
240 debug!("flash save done");
241 Ok(())
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use crate::settings::KEY_SLOTS;
248 use core::str::FromStr;
249 use embedded_storage::nor_flash::{ErrorType, NorFlashErrorKind, ReadNorFlash};
250 use embedded_storage_inmemory::MemFlash;
251 use heapless::String;
252 use sunset::packets::Ed25519PubKey;
253 use sunset::sshwire::Blob;
254
255 type TestFlash = MemFlash<{ CONFIG_OFFSET + CONFIG_AREA_SIZE }, CONFIG_AREA_SIZE, 4>;
256
257 fn round_trip(config: &SSHStampConfig) {
258 let mut flash = TestFlash::new(0);
259 let mut buf = [0u8; CONFIG_AREA_SIZE];
260 save(&mut flash, &mut buf, config).unwrap();
261 let loaded = load(&mut flash, &mut buf).unwrap();
262 assert_eq!(&loaded, config);
263 }
264
265 fn test_config() -> SSHStampConfig {
266 SSHStampConfig::new([0x02; 6], UartPins { rx: 10, tx: 11 }).unwrap()
267 }
268
269 #[test]
270 fn config_with_round_trip() {
271 round_trip(&test_config());
272 }
273
274 #[test]
275 fn config_with_wifi_round_trip() {
276 let mut config = test_config();
277 config.pubkeys = [Some(Ed25519PubKey {
278 key: Blob([0x5a; 32]),
279 }); KEY_SLOTS];
280 config.wifi_sta_ssid = String::from_str(&"s".repeat(32)).unwrap();
281 config.wifi_sta_pw = String::from_str(&"p".repeat(63)).unwrap();
282
283 round_trip(&config);
284
285 let mut buf = [0u8; CONFIG_AREA_SIZE];
286 let written = sshwire::write_ssh(
287 &mut buf,
288 &FlashConfig {
289 version: SSHStampConfig::CURRENT_VERSION,
290 config: OwnOrBorrow::Borrow(&config),
291 hash: config_hash(&config).unwrap(),
292 },
293 )
294 .unwrap();
295 assert!(written <= FlashConfig::BUF_SIZE);
296 assert!(written <= CONFIG_AREA_SIZE);
297 }
298
299 const MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01];
300 const WRITE_GRANULARITY: usize = 4;
302 const FLASH_LEN: usize = CONFIG_OFFSET + 4 * CONFIG_AREA_SIZE;
305 const STALE: u8 = 0xAA;
308
309 fn pins() -> UartPins {
310 UartPins { rx: 4, tx: 5 }
311 }
312
313 struct MockFlash {
317 cells: std::vec::Vec<u8>,
318 }
319
320 impl MockFlash {
321 fn erased() -> Self {
322 Self {
323 cells: std::vec![0xFF; FLASH_LEN],
324 }
325 }
326
327 fn config_area(&self) -> &[u8] {
328 &self.cells[CONFIG_OFFSET..CONFIG_OFFSET + CONFIG_AREA_SIZE]
329 }
330
331 fn past_config_area(&self) -> &[u8] {
332 &self.cells[CONFIG_OFFSET + CONFIG_AREA_SIZE..]
333 }
334
335 fn bounds(&self, offset: u32, len: usize) -> Result<(usize, usize), NorFlashErrorKind> {
336 let start = offset as usize;
337 let end = start
338 .checked_add(len)
339 .ok_or(NorFlashErrorKind::OutOfBounds)?;
340 if end > self.cells.len() {
341 return Err(NorFlashErrorKind::OutOfBounds);
342 }
343 Ok((start, end))
344 }
345 }
346
347 impl ErrorType for MockFlash {
348 type Error = NorFlashErrorKind;
349 }
350
351 impl ReadNorFlash for MockFlash {
352 const READ_SIZE: usize = 1;
353
354 fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> {
355 let (start, end) = self.bounds(offset, bytes.len())?;
356 bytes.copy_from_slice(&self.cells[start..end]);
357 Ok(())
358 }
359
360 fn capacity(&self) -> usize {
361 self.cells.len()
362 }
363 }
364
365 impl NorFlash for MockFlash {
366 const WRITE_SIZE: usize = WRITE_GRANULARITY;
367 const ERASE_SIZE: usize = CONFIG_AREA_SIZE;
368
369 fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> {
370 let (start, end) = self.bounds(from, (to - from) as usize)?;
371 self.cells[start..end].fill(0xFF);
372 Ok(())
373 }
374
375 fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
376 if !(offset as usize).is_multiple_of(Self::WRITE_SIZE)
377 || !bytes.len().is_multiple_of(Self::WRITE_SIZE)
378 {
379 return Err(NorFlashErrorKind::NotAligned);
380 }
381 let (start, end) = self.bounds(offset, bytes.len())?;
382 for (cell, byte) in self.cells[start..end].iter_mut().zip(bytes) {
383 *cell &= byte;
384 }
385 Ok(())
386 }
387 }
388
389 fn encoded_len(config: &SSHStampConfig) -> usize {
392 let sc = FlashConfig {
393 version: SSHStampConfig::CURRENT_VERSION,
394 config: OwnOrBorrow::Borrow(config),
395 hash: config_hash(config).unwrap(),
396 };
397 let mut probe = [0u8; CONFIG_AREA_SIZE];
398 sshwire::write_ssh(&mut probe, &sc).unwrap()
399 }
400
401 #[test]
404 fn blank_flash_mints_a_config() {
405 let mut flash = MockFlash::erased();
406 let mut buf = [0u8; CONFIG_AREA_SIZE];
407
408 let created = load_or_create(&mut flash, &mut buf, MAC, pins())
409 .expect("first boot on erased flash must create a config");
410 assert!(created.first_login);
411
412 let mut buf = [0u8; CONFIG_AREA_SIZE];
413 let reloaded = load(&mut flash, &mut buf).expect("the config just written must load back");
414 assert_eq!(created, reloaded);
415 }
416
417 #[test]
421 fn wrong_version_is_refused_without_overwriting() {
422 let mut flash = MockFlash::erased();
423 let mut buf = [0u8; CONFIG_AREA_SIZE];
424 load_or_create(&mut flash, &mut buf, MAC, pins()).unwrap();
425
426 flash.cells[CONFIG_OFFSET] = SSHStampConfig::CURRENT_VERSION.wrapping_add(1);
428 let before = flash.cells.clone();
429
430 let mut buf = [0u8; CONFIG_AREA_SIZE];
431 assert!(
432 load_or_create(&mut flash, &mut buf, MAC, pins()).is_err(),
433 "a version mismatch must fail closed, not mint a new config"
434 );
435 assert_eq!(flash.cells, before, "flash must be left untouched");
436 }
437
438 #[test]
440 fn bad_hash_is_refused_without_overwriting() {
441 let mut flash = MockFlash::erased();
442 let mut buf = [0u8; CONFIG_AREA_SIZE];
443 let config = load_or_create(&mut flash, &mut buf, MAC, pins()).unwrap();
444
445 let hash_start = CONFIG_OFFSET + encoded_len(&config) - CONFIG_HASH_SIZE;
447 flash.cells[hash_start] ^= 0xFF;
448 let before = flash.cells.clone();
449
450 let mut buf = [0u8; CONFIG_AREA_SIZE];
451 assert!(
452 load_or_create(&mut flash, &mut buf, MAC, pins()).is_err(),
453 "a hash mismatch must fail closed, not mint a new config"
454 );
455 assert_eq!(flash.cells, before, "flash must be left untouched");
456 }
457
458 #[test]
461 fn save_does_not_persist_stale_buffer_bytes() {
462 let mut flash = MockFlash::erased();
463 let mut buf = [STALE; CONFIG_AREA_SIZE];
464 let config = SSHStampConfig::new(MAC, pins()).unwrap();
465
466 save(&mut flash, &mut buf, &config).unwrap();
467
468 let written = encoded_len(&config).next_multiple_of(WRITE_GRANULARITY);
469 assert!(
470 flash.config_area()[written..].iter().all(|&b| b == 0xFF),
471 "bytes past the encoded config were written to flash"
472 );
473 let mut buf = [0u8; CONFIG_AREA_SIZE];
476 assert_eq!(load(&mut flash, &mut buf).unwrap(), config);
477 }
478
479 #[test]
482 fn save_stays_within_the_config_area() {
483 let mut flash = MockFlash::erased();
484 let mut buf = [STALE; CONFIG_AREA_SIZE * 2];
485 let config = SSHStampConfig::new(MAC, pins()).unwrap();
486
487 save(&mut flash, &mut buf, &config).unwrap();
488
489 assert!(
490 flash.past_config_area().iter().all(|&b| b == 0xFF),
491 "save wrote past CONFIG_AREA_SIZE into the adjacent partition"
492 );
493 }
494}