diff --git a/CHANGELOG.md b/CHANGELOG.md index 317605bc..2b185cc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support external log-processors (#705) - Make the `libudev` dependency optional with a new - enabled by default - feature: `libudev` (#709) - Address Clippy lints (#710) +- Add support for flash-encryption (#718) ### Changed diff --git a/cargo-espflash/src/main.rs b/cargo-espflash/src/main.rs index 1c1e3633..db9b98d8 100644 --- a/cargo-espflash/src/main.rs +++ b/cargo-espflash/src/main.rs @@ -190,6 +190,8 @@ struct FlashArgs { connect_args: ConnectArgs, #[clap(flatten)] flash_args: cli::FlashArgs, + #[arg(long)] + encrypt: bool, } #[derive(Debug, Args)] @@ -336,6 +338,7 @@ fn flash(args: FlashArgs, config: &Config) -> Result<()> { config, build_ctx.bootloader_path.as_deref(), build_ctx.partition_table_path.as_deref(), + args.flash_args.encrypt, )?; if args.flash_args.erase_parts.is_some() || args.flash_args.erase_data_parts.is_some() { @@ -579,6 +582,7 @@ fn save_image(args: SaveImageArgs, config: &Config) -> Result<()> { config, build_ctx.bootloader_path.as_deref(), build_ctx.partition_table_path.as_deref(), + false, // We don't care about encryption when writing a .bin, as it is not stored in there )?; let xtal_freq = args diff --git a/espflash/src/bin/espflash.rs b/espflash/src/bin/espflash.rs index f117b9ce..4c6fd34f 100644 --- a/espflash/src/bin/espflash.rs +++ b/espflash/src/bin/espflash.rs @@ -154,6 +154,8 @@ struct WriteBinArgs { /// Connection configuration #[clap(flatten)] connect_args: ConnectArgs, + #[arg(long)] + encrypt: bool, } fn main() -> Result<()> { @@ -271,6 +273,7 @@ fn flash(args: FlashArgs, config: &Config) -> Result<()> { config, None, None, + args.flash_args.encrypt, )?; if args.flash_args.erase_parts.is_some() || args.flash_args.erase_data_parts.is_some() { @@ -328,6 +331,7 @@ fn save_image(args: SaveImageArgs, config: &Config) -> Result<()> { config, None, None, + false, // We don't care about encryption when writing a .bin, as it is not stored in there )?; let xtal_freq = args @@ -357,7 +361,12 @@ fn write_bin(args: WriteBinArgs, config: &Config) -> Result<()> { let mut buffer = Vec::with_capacity(size.try_into().into_diagnostic()?); f.read_to_end(&mut buffer).into_diagnostic()?; - flasher.write_bin_to_flash(args.addr, &buffer, Some(&mut EspflashProgress::default()))?; + flasher.write_bin_to_flash( + args.addr, + &buffer, + Some(&mut EspflashProgress::default()), + args.encrypt, + )?; Ok(()) } diff --git a/espflash/src/cli/mod.rs b/espflash/src/cli/mod.rs index b4825ce9..1aa19585 100644 --- a/espflash/src/cli/mod.rs +++ b/espflash/src/cli/mod.rs @@ -159,6 +159,8 @@ pub struct FlashArgs { /// External log processors to use (comma separated executables) #[arg(long, requires = "monitor")] pub processors: Option, + #[arg(long)] + pub encrypt: bool, } /// Operations for partitions tables @@ -822,6 +824,7 @@ pub fn make_flash_data( config: &Config, default_bootloader: Option<&Path>, default_partition_table: Option<&Path>, + encrypted: bool, ) -> Result { let bootloader = image_args .bootloader @@ -853,6 +856,7 @@ pub fn make_flash_data( image_args.target_app_partition, flash_settings, image_args.min_chip_rev, + encrypted, ) } diff --git a/espflash/src/command.rs b/espflash/src/command.rs index be91e504..851369c6 100644 --- a/espflash/src/command.rs +++ b/espflash/src/command.rs @@ -1,6 +1,10 @@ //! Commands to work with a flasher stub running on a target device -use std::{io::Write, mem::size_of, time::Duration}; +use std::{ + io::{ErrorKind, Write}, + mem::size_of, + time::Duration, +}; use bytemuck::{bytes_of, Pod, Zeroable}; use strum::Display; @@ -106,6 +110,7 @@ pub enum Command<'a> { block_size: u32, offset: u32, supports_encryption: bool, + encrypt: bool, }, FlashData { data: &'a [u8], @@ -237,6 +242,7 @@ impl Command<'_> { block_size, offset, supports_encryption, + encrypt, } => { begin_command( writer, @@ -245,6 +251,7 @@ impl Command<'_> { block_size, offset, supports_encryption, + encrypt, )?; } Command::FlashData { @@ -272,6 +279,7 @@ impl Command<'_> { block_size, offset, supports_encryption, + false, )?; } Command::MemData { @@ -360,6 +368,7 @@ impl Command<'_> { block_size, offset, supports_encryption, + false, // Compression and encryption are mutually exclusive )?; } Command::FlashDeflData { @@ -441,7 +450,15 @@ fn begin_command( block_size: u32, offset: u32, supports_encryption: bool, + encrypt: bool, ) -> std::io::Result<()> { + if encrypt && !supports_encryption { + return Err(std::io::Error::new( + ErrorKind::InvalidInput, + "Target does not support encryption, yet encryption is requested", + )); + } + #[derive(Zeroable, Pod, Copy, Clone, Debug)] #[repr(C)] struct BeginParams { @@ -456,7 +473,7 @@ fn begin_command( blocks, block_size, offset, - encrypted: 0, + encrypted: encrypt as u32, }; let bytes = bytes_of(¶ms); diff --git a/espflash/src/connection/reset.rs b/espflash/src/connection/reset.rs index 936f232a..0581663f 100644 --- a/espflash/src/connection/reset.rs +++ b/espflash/src/connection/reset.rs @@ -260,6 +260,7 @@ pub fn soft_reset( block_size: FLASH_WRITE_SIZE.try_into().unwrap(), offset, supports_encryption: false, + encrypt: false, }) })?; connection.with_timeout(CommandType::FlashEnd.timeout(), |connection| { @@ -278,6 +279,7 @@ pub fn soft_reset( block_size: FLASH_WRITE_SIZE.try_into().unwrap(), offset, supports_encryption: false, + encrypt: false, }) })?; connection.with_timeout(CommandType::FlashEnd.timeout(), |connection| { diff --git a/espflash/src/elf.rs b/espflash/src/elf.rs index 3f0f3e34..e5d085cc 100644 --- a/espflash/src/elf.rs +++ b/espflash/src/elf.rs @@ -237,6 +237,8 @@ pub struct RomSegment<'a> { pub addr: u32, /// Segment data pub data: Cow<'a, [u8]>, + /// Whether the segment shall be encrypted before being writen + pub encrypted: bool, } impl<'a> RomSegment<'a> { @@ -247,15 +249,15 @@ impl<'a> RomSegment<'a> { RomSegment { addr: self.addr, data: Cow::Borrowed(self.data.as_ref()), + encrypted: self.encrypted, } } -} -impl<'a> From> for RomSegment<'a> { - fn from(segment: CodeSegment<'a>) -> Self { + pub fn from_code_segment(segment: &CodeSegment<'a>, encrypted: bool) -> Self { RomSegment { addr: segment.addr, - data: segment.data, + data: segment.data.clone(), + encrypted, } } } diff --git a/espflash/src/flasher/mod.rs b/espflash/src/flasher/mod.rs index d57fdf55..52cad598 100644 --- a/espflash/src/flasher/mod.rs +++ b/espflash/src/flasher/mod.rs @@ -300,6 +300,7 @@ pub struct FlashDataBuilder<'a> { target_app_partition: Option, flash_settings: FlashSettings, min_chip_rev: u16, + encryption: bool, } impl Default for FlashDataBuilder<'_> { @@ -311,6 +312,7 @@ impl Default for FlashDataBuilder<'_> { target_app_partition: Default::default(), flash_settings: FlashSettings::default(), min_chip_rev: Default::default(), + encryption: Default::default(), } } } @@ -357,6 +359,12 @@ impl<'a> FlashDataBuilder<'a> { self } + /// Sets the minimum chip revision. + pub fn with_encryption(mut self, encryption: bool) -> Self { + self.encryption = encryption; + self + } + /// Builds a [`FlashData`] object. pub fn build(self) -> Result { FlashData::new( @@ -366,6 +374,7 @@ impl<'a> FlashDataBuilder<'a> { self.target_app_partition, self.flash_settings, self.min_chip_rev, + self.encryption, ) } } @@ -380,6 +389,7 @@ pub struct FlashData { pub target_app_partition: Option, pub flash_settings: FlashSettings, pub min_chip_rev: u16, + pub encrypted: bool, } impl FlashData { @@ -390,6 +400,7 @@ impl FlashData { target_app_partition: Option, flash_settings: FlashSettings, min_chip_rev: u16, + encrypted: bool, ) -> Result { // If the '--bootloader' option is provided, load the binary file at the // specified path. @@ -417,6 +428,7 @@ impl FlashData { target_app_partition, flash_settings, min_chip_rev, + encrypted, }) } } @@ -643,9 +655,9 @@ impl Flasher { } pub fn disable_watchdog(&mut self) -> Result<(), Error> { - let mut target = self - .chip - .flash_target(self.spi_params, self.use_stub, false, false); + let mut target = + self.chip + .flash_target(self.spi_params, self.use_stub, false, false, false); target.begin(&mut self.connection).flashing()?; Ok(()) } @@ -674,6 +686,7 @@ impl Flasher { RomSegment { addr: text_addr, data: Cow::Borrowed(&text), + encrypted: false, }, &mut None, ) @@ -688,6 +701,7 @@ impl Flasher { RomSegment { addr: data_addr, data: Cow::Borrowed(&data), + encrypted: false, }, &mut None, ) @@ -939,7 +953,11 @@ impl Flasher { for segment in image.ram_segments(self.chip) { target - .write_segment(&mut self.connection, segment.into(), &mut progress) + .write_segment( + &mut self.connection, + RomSegment::from_code_segment(&segment, false), + &mut progress, + ) .flashing()?; } @@ -956,9 +974,13 @@ impl Flasher { ) -> Result<(), Error> { let image = ElfFirmwareImage::try_from(elf_data)?; - let mut target = - self.chip - .flash_target(self.spi_params, self.use_stub, self.verify, self.skip); + let mut target = self.chip.flash_target( + self.spi_params, + self.use_stub, + self.verify, + self.skip, + flash_data.encrypted, + ); target.begin(&mut self.connection).flashing()?; let chip_revision = Some( @@ -995,10 +1017,12 @@ impl Flasher { addr: u32, data: &[u8], progress: Option<&mut dyn ProgressCallbacks>, + encrypt: bool, ) -> Result<(), Error> { let segment = RomSegment { addr, data: Cow::from(data), + encrypted: encrypt, }; self.write_bins_to_flash(&[segment], progress)?; @@ -1013,9 +1037,10 @@ impl Flasher { segments: &[RomSegment], mut progress: Option<&mut dyn ProgressCallbacks>, ) -> Result<(), Error> { - let mut target = self - .chip - .flash_target(self.spi_params, self.use_stub, false, false); + let encrypt = segments.iter().any(|seg| seg.encrypted); + let mut target = + self.chip + .flash_target(self.spi_params, self.use_stub, false, false, encrypt); target.begin(&mut self.connection).flashing()?; for segment in segments { target.write_segment(&mut self.connection, segment.borrow(), &mut progress)?; diff --git a/espflash/src/image_format.rs b/espflash/src/image_format.rs index 2af83ece..c851cea6 100644 --- a/espflash/src/image_format.rs +++ b/espflash/src/image_format.rs @@ -124,6 +124,7 @@ impl<'a> IdfBootloaderFormat<'a> { target_app_partition: Option, bootloader: Option>, flash_settings: FlashSettings, + encrypted: bool, ) -> Result { let partition_table = partition_table.unwrap_or_else(|| { params.default_partition_table(flash_settings.size.map(|v| v.size())) @@ -269,6 +270,7 @@ impl<'a> IdfBootloaderFormat<'a> { let flash_segment = RomSegment { addr: target_app_partition.offset(), data: Cow::Owned(data), + encrypted, }; // If the user did not specify a partition offset, we need to assume that the @@ -298,19 +300,25 @@ impl<'a> IdfBootloaderFormat<'a> { where 'a: 'b, { + // Flash encryption, if enabled, will automatically be enabled for those 3 partitions: + // https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32s3/security/flash-encryption.html#encrypted-partitions + let bootloader_segment = RomSegment { addr: self.params.boot_addr, data: Cow::Borrowed(&self.bootloader), + encrypted: self.flash_segment.encrypted, }; let partition_table_segment = RomSegment { addr: self.partition_table_offset, data: Cow::Owned(self.partition_table.to_bin().unwrap()), + encrypted: self.flash_segment.encrypted, }; let app_segment = RomSegment { addr: self.flash_segment.addr, data: Cow::Borrowed(&self.flash_segment.data), + encrypted: self.flash_segment.encrypted, }; Box::new( diff --git a/espflash/src/targets/esp32.rs b/espflash/src/targets/esp32.rs index edcfefe2..a82e384f 100644 --- a/espflash/src/targets/esp32.rs +++ b/espflash/src/targets/esp32.rs @@ -159,7 +159,7 @@ impl Target for Esp32 { _chip_revision: Option<(u32, u32)>, xtal_freq: XtalFrequency, ) -> Result, Error> { - let booloader: &'static [u8] = match xtal_freq { + let bootloader: &'static [u8] = match xtal_freq { XtalFrequency::_40Mhz => { include_bytes!("../../resources/bootloaders/esp32-bootloader.bin") } @@ -180,7 +180,7 @@ impl Target for Esp32 { 0x3f_0000, 0, FlashFrequency::_40Mhz, - booloader, + bootloader, ); IdfBootloaderFormat::new( @@ -193,6 +193,7 @@ impl Target for Esp32 { flash_data.target_app_partition, flash_data.bootloader, flash_data.flash_settings, + flash_data.encrypted, ) } diff --git a/espflash/src/targets/esp32c2.rs b/espflash/src/targets/esp32c2.rs index a307e318..ce47df17 100644 --- a/espflash/src/targets/esp32c2.rs +++ b/espflash/src/targets/esp32c2.rs @@ -131,6 +131,7 @@ impl Target for Esp32c2 { flash_data.target_app_partition, flash_data.bootloader, flash_data.flash_settings, + flash_data.encrypted, ) } diff --git a/espflash/src/targets/esp32c3.rs b/espflash/src/targets/esp32c3.rs index 31f7cfbf..bbb1ef8f 100644 --- a/espflash/src/targets/esp32c3.rs +++ b/espflash/src/targets/esp32c3.rs @@ -100,6 +100,7 @@ impl Target for Esp32c3 { flash_data.target_app_partition, flash_data.bootloader, flash_data.flash_settings, + flash_data.encrypted, ) } diff --git a/espflash/src/targets/esp32c6.rs b/espflash/src/targets/esp32c6.rs index 082bfcb9..dce86d9c 100644 --- a/espflash/src/targets/esp32c6.rs +++ b/espflash/src/targets/esp32c6.rs @@ -95,6 +95,7 @@ impl Target for Esp32c6 { flash_data.target_app_partition, flash_data.bootloader, flash_data.flash_settings, + flash_data.encrypted, ) } diff --git a/espflash/src/targets/esp32h2.rs b/espflash/src/targets/esp32h2.rs index c0a3ec57..3f7c9dd5 100644 --- a/espflash/src/targets/esp32h2.rs +++ b/espflash/src/targets/esp32h2.rs @@ -102,6 +102,7 @@ impl Target for Esp32h2 { flash_data.target_app_partition, flash_data.bootloader, flash_data.flash_settings, + flash_data.encrypted, ) } diff --git a/espflash/src/targets/esp32p4.rs b/espflash/src/targets/esp32p4.rs index 73b2f5ac..40ca76c7 100644 --- a/espflash/src/targets/esp32p4.rs +++ b/espflash/src/targets/esp32p4.rs @@ -91,6 +91,7 @@ impl Target for Esp32p4 { flash_data.target_app_partition, flash_data.bootloader, flash_data.flash_settings, + flash_data.encrypted, ) } diff --git a/espflash/src/targets/esp32s2.rs b/espflash/src/targets/esp32s2.rs index 804893c3..a66fc8a7 100644 --- a/espflash/src/targets/esp32s2.rs +++ b/espflash/src/targets/esp32s2.rs @@ -169,6 +169,7 @@ impl Target for Esp32s2 { flash_data.target_app_partition, flash_data.bootloader, flash_data.flash_settings, + flash_data.encrypted, ) } diff --git a/espflash/src/targets/esp32s3.rs b/espflash/src/targets/esp32s3.rs index 59798c69..ec0811a2 100644 --- a/espflash/src/targets/esp32s3.rs +++ b/espflash/src/targets/esp32s3.rs @@ -119,6 +119,7 @@ impl Target for Esp32s3 { flash_data.target_app_partition, flash_data.bootloader, flash_data.flash_settings, + flash_data.encrypted, ) } diff --git a/espflash/src/targets/flash_target/esp32.rs b/espflash/src/targets/flash_target/esp32.rs index e6088d47..c7188fdd 100644 --- a/espflash/src/targets/flash_target/esp32.rs +++ b/espflash/src/targets/flash_target/esp32.rs @@ -1,4 +1,4 @@ -use std::io::Write; +use std::{borrow::Cow, io::Write}; use flate2::{ write::{ZlibDecoder, ZlibEncoder}, @@ -28,7 +28,8 @@ pub struct Esp32Target { use_stub: bool, verify: bool, skip: bool, - need_deflate_end: bool, + encrypt: bool, + need_transfer_end: bool, } impl Esp32Target { @@ -38,6 +39,7 @@ impl Esp32Target { use_stub: bool, verify: bool, skip: bool, + encrypt: bool, ) -> Self { Esp32Target { chip, @@ -45,7 +47,8 @@ impl Esp32Target { use_stub, verify, skip, - need_deflate_end: false, + encrypt, + need_transfer_end: false, } } } @@ -144,7 +147,7 @@ impl FlashTarget for Esp32Target { md5_hasher.update(&segment.data); let checksum_md5 = md5_hasher.finalize(); - if self.skip { + if self.skip && !segment.encrypted { let flash_checksum_md5: u128 = connection.with_timeout(CommandType::FlashMd5.timeout(), |connection| { connection @@ -164,62 +167,106 @@ impl FlashTarget for Esp32Target { } } - let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best()); - encoder.write_all(&segment.data)?; - let compressed = encoder.finish()?; - let target = self.chip.into_target(); let flash_write_size = target.flash_write_size(connection)?; - let block_count = compressed.len().div_ceil(flash_write_size); let erase_count = segment.data.len().div_ceil(FLASH_SECTOR_SIZE); - - // round up to sector size + // round erase up to sector size let erase_size = (erase_count * FLASH_SECTOR_SIZE) as u32; + let payload: Cow<[u8]> = if self.encrypt { + Cow::Borrowed(&segment.data) + } else { + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best()); + encoder.write_all(&segment.data)?; + let compressed = encoder.finish()?; + Cow::Owned(compressed) + }; + let block_count = payload.len().div_ceil(flash_write_size); + if self.encrypt { + connection.with_timeout( + CommandType::FlashBegin.timeout_for_size(erase_size), + |connection| { + connection.command(Command::FlashBegin { + size: segment.data.len() as u32, + blocks: block_count as u32, + block_size: flash_write_size as u32, + offset: addr, + supports_encryption: self.chip != Chip::Esp32 && !self.use_stub, + encrypt: segment.encrypted, + })?; + Ok(()) + }, + )?; + } else { + connection.with_timeout( + CommandType::FlashDeflBegin.timeout_for_size(erase_size), + |connection| { + connection.command(Command::FlashDeflBegin { + size: segment.data.len() as u32, + blocks: block_count as u32, + block_size: flash_write_size as u32, + offset: addr, + supports_encryption: self.chip != Chip::Esp32 && !self.use_stub, + })?; + Ok(()) + }, + )?; + } + self.need_transfer_end = true; - connection.with_timeout( - CommandType::FlashDeflBegin.timeout_for_size(erase_size), - |connection| { - connection.command(Command::FlashDeflBegin { - size: segment.data.len() as u32, - blocks: block_count as u32, - block_size: flash_write_size as u32, - offset: addr, - supports_encryption: self.chip != Chip::Esp32 && !self.use_stub, - })?; - Ok(()) - }, - )?; - self.need_deflate_end = true; - - let chunks = compressed.chunks(flash_write_size); + let chunks = payload.chunks(flash_write_size); let num_chunks = chunks.len(); if let Some(cb) = progress.as_mut() { cb.init(addr, num_chunks) } - // decode the chunks to see how much data the device will have to save - let mut decoder = ZlibDecoder::new(Vec::new()); - let mut decoded_size = 0; + // Operation timeout is based on flash operation duration. + // When using compressed transfers, we thus need to deflate to know + // how many bytes will be written / erased, + // and thus how long the timeout will be + let mut decoder = if !self.encrypt { + Some(ZlibDecoder::new(Vec::new())) + } else { + None + }; for (i, block) in chunks.enumerate() { - decoder.write_all(block)?; - decoder.flush()?; - let size = decoder.get_ref().len() - decoded_size; - decoded_size = decoder.get_ref().len(); + let chunk_size_in_flash = if let Some(decoder) = &mut decoder { + let previous_length = decoder.get_ref().len(); + decoder.write_all(block)?; + decoder.flush()?; + decoder.get_ref().len() - previous_length + } else { + block.len() + }; - connection.with_timeout( - CommandType::FlashDeflData.timeout_for_size(size as u32), - |connection| { - connection.command(Command::FlashDeflData { - sequence: i as u32, - pad_to: 0, - pad_byte: 0xff, - data: block, - })?; - Ok(()) - }, - )?; + if self.encrypt { + connection.with_timeout( + CommandType::FlashData.timeout_for_size(chunk_size_in_flash as u32), + |connection| { + connection.command(Command::FlashData { + sequence: i as u32, + pad_to: 0, + pad_byte: 0xff, + data: block, + })?; + Ok(()) + }, + )?; + } else { + connection.with_timeout( + CommandType::FlashDeflData.timeout_for_size(chunk_size_in_flash as u32), + |connection| { + connection.command(Command::FlashDeflData { + sequence: i as u32, + pad_to: 0, + pad_byte: 0xff, + data: block, + })?; + Ok(()) + }, + )?; + } if let Some(cb) = progress.as_mut() { cb.update(i + 1) @@ -230,7 +277,7 @@ impl FlashTarget for Esp32Target { cb.finish() } - if self.verify { + if self.verify && !segment.encrypted { let flash_checksum_md5: u128 = connection.with_timeout(CommandType::FlashMd5.timeout(), |connection| { connection @@ -250,10 +297,16 @@ impl FlashTarget for Esp32Target { } fn finish(&mut self, connection: &mut Connection, reboot: bool) -> Result<(), Error> { - if self.need_deflate_end { - connection.with_timeout(CommandType::FlashDeflEnd.timeout(), |connection| { - connection.command(Command::FlashDeflEnd { reboot: false }) - })?; + if self.need_transfer_end { + if self.encrypt { + connection.with_timeout(CommandType::FlashEnd.timeout(), |connection| { + connection.command(Command::FlashEnd { reboot: false }) + })?; + } else { + connection.with_timeout(CommandType::FlashDeflEnd.timeout(), |connection| { + connection.command(Command::FlashDeflEnd { reboot: false }) + })?; + } } if reboot { diff --git a/espflash/src/targets/mod.rs b/espflash/src/targets/mod.rs index 05e5bf9a..5fa1aebf 100644 --- a/espflash/src/targets/mod.rs +++ b/espflash/src/targets/mod.rs @@ -150,8 +150,11 @@ impl Chip { use_stub: bool, verify: bool, skip: bool, + encrypt: bool, ) -> Box { - Box::new(Esp32Target::new(*self, spi_params, use_stub, verify, skip)) + Box::new(Esp32Target::new( + *self, spi_params, use_stub, verify, skip, encrypt, + )) } #[cfg(feature = "serialport")]