diff --git a/mla/src/errors.rs b/mla/src/errors.rs index 663963cc..bc2be3fa 100644 --- a/mla/src/errors.rs +++ b/mla/src/errors.rs @@ -92,7 +92,12 @@ impl From for Error { impl From for io::Error { fn from(error: Error) -> Self { - io::Error::new(io::ErrorKind::Other, format!("{error}")) + match error { + // On IOError, unwrap it (MLAError(IOError(err))) -> err + Error::IOError(err) => err, + // Otherwise, use a generic construction + _ => io::Error::new(io::ErrorKind::Other, format!("{error}")), + } } } diff --git a/mla/src/layers/encrypt.rs b/mla/src/layers/encrypt.rs index 8beabd9c..d1bb2b73 100644 --- a/mla/src/layers/encrypt.rs +++ b/mla/src/layers/encrypt.rs @@ -49,6 +49,7 @@ pub struct EncryptionPersistentConfig { nonce: [u8; NONCE_SIZE], } +/// Specific config for ArchiveWriter pub struct EncryptionConfig { /// Public keys with which to encrypt the symmetric encryption key below ecc_keys: Vec, @@ -129,12 +130,24 @@ impl ArchiveWriterConfig { } } +/// FailSafeReader decryption mode +#[derive(Default, Clone, Copy)] +enum FailSafeReaderDecryptionMode { + /// Returns only the data that have been authenticated on decryption + #[default] + OnlyAuthenticatedData, + /// Returns all data, even if not authenticated + DataEvenUnauthenticated, +} + #[derive(Default)] pub struct EncryptionReaderConfig { /// Private key(s) to use private_keys: Vec, /// Symmetric encryption key and nonce, if decrypted successfully from header encrypt_parameters: Option<(Key, [u8; NONCE_SIZE])>, + /// FailSafeReader consideration for tag on decryption -- FailSafeReader only + failsafe_mode: FailSafeReaderDecryptionMode, } impl EncryptionReaderConfig { @@ -175,6 +188,18 @@ impl ArchiveReaderConfig { pub fn get_encrypt_parameters(&self) -> Option<(Key, [u8; NONCE_SIZE])> { self.encrypt.encrypt_parameters } + + /// Set the FailSafeReader decryption mode to return only authenticated data (default) + pub fn failsafe_return_only_authenticated_data(&mut self) -> &mut ArchiveReaderConfig { + self.encrypt.failsafe_mode = FailSafeReaderDecryptionMode::OnlyAuthenticatedData; + self + } + + /// Set the FailSafeReader decryption mode to return all data, even if not authenticated + pub fn failsafe_return_data_even_unauthenticated(&mut self) -> &mut ArchiveReaderConfig { + self.encrypt.failsafe_mode = FailSafeReaderDecryptionMode::DataEvenUnauthenticated; + self + } } // ---------- Writer ---------- @@ -273,8 +298,10 @@ impl<'a, W: InnerWriterTrait> Write for EncryptionLayerWriter<'a, W> { // In the case of stream cipher, encrypting is the same that decrypting. Here, we // keep the struct separated for any possible future difference -pub struct EncryptionLayerReader<'a, R: Read + Seek> { - inner: Box>, + +/// This struct permits code reuse on layer read between "normal" read and "failsafe" read +struct EncryptionLayerInternal { + inner: Box, cipher: AesGcm256, key: Key, nonce: [u8; NONCE_SIZE], @@ -282,11 +309,8 @@ pub struct EncryptionLayerReader<'a, R: Read + Seek> { current_chunk_number: u32, } -impl<'a, R: 'a + Read + Seek> EncryptionLayerReader<'a, R> { - pub fn new( - inner: Box>, - config: &EncryptionReaderConfig, - ) -> Result { +impl EncryptionLayerInternal { + pub fn new(inner: Box, config: &EncryptionReaderConfig) -> Result { match config.encrypt_parameters { Some((key, nonce)) => Ok(Self { inner, @@ -299,7 +323,9 @@ impl<'a, R: 'a + Read + Seek> EncryptionLayerReader<'a, R> { None => Err(Error::PrivateKeyNeeded), } } +} +impl EncryptionLayerInternal { /// Load the `self.current_chunk_number` chunk in cache /// Assume the inner layer is in the correct position fn load_in_cache(&mut self) -> Result, Error> { @@ -340,29 +366,46 @@ impl<'a, R: 'a + Read + Seek> EncryptionLayerReader<'a, R> { Ok(Some(())) } } -} -impl<'a, R: 'a + InnerReaderTrait> LayerReader<'a, R> for EncryptionLayerReader<'a, R> { - fn into_inner(self) -> Option>> { - Some(self.inner) - } + /// Load in cache the decrypted data, without checking the authentication tag + /// + /// Assumption: + /// - the inner layer is at the start of a CHUNK + fn load_in_cache_unauthenticated(&mut self) -> Result, Error> { + self.cipher = AesGcm256::new( + &self.key, + &build_nonce(self.nonce, self.current_chunk_number), + b"", + )?; - fn into_raw(self: Box) -> R { - self.inner.into_raw() - } + // Clear current, now useless, allocated memory + self.chunk_cache.get_mut().clear(); - fn initialize(&mut self) -> Result<(), Error> { - // Recursive call - self.inner.initialize()?; + // Load the current encrypted chunk in memory + let mut data = Vec::with_capacity(CHUNK_SIZE as usize); + let data_read = (&mut self.inner).take(CHUNK_SIZE).read_to_end(&mut data)?; + // If the inner is at the end of the stream, we cannot read any + // additional byte -> we must stop + if data_read == 0 { + return Ok(None); + } - // Load the current buffer in cache - self.rewind()?; - Ok(()) + // Consume the tag without reading it + io::copy( + &mut (&mut self.inner).take(TAG_LENGTH as u64), + &mut io::sink(), + )?; + + // Decrypt the current chunk + self.cipher.decrypt_unauthenticated(data.as_mut_slice()); + self.chunk_cache = Cursor::new(data); + Ok(Some(())) } -} -impl<'a, R: 'a + Read + Seek> Read for EncryptionLayerReader<'a, R> { - fn read(&mut self, buf: &mut [u8]) -> io::Result { + /// Internal `Read`::read but returning a mla `Error` + /// + /// This method check the tag of each decrypted block + fn read_internal(&mut self, buf: &mut [u8]) -> Result { let cache_to_consume = CHUNK_SIZE - self.chunk_cache.position(); if cache_to_consume == 0 { // Cache totally consumed, renew it @@ -371,32 +414,43 @@ impl<'a, R: 'a + Read + Seek> Read for EncryptionLayerReader<'a, R> { // No more byte in the inner layer return Ok(0); } - return self.read(buf); + return self.read_internal(buf); } // Consume at most the bytes leaving in the cache, to detect the renewal need let size = std::cmp::min(cache_to_consume as usize, buf.len()); - self.chunk_cache.read(&mut buf[..size]) + self.chunk_cache + .read(&mut buf[..size]) + .map_err(|e| e.into()) } -} - -// Returns how many chunk are present at position `position` -const CHUNK_TAG_SIZE: u64 = CHUNK_SIZE + TAG_LENGTH as u64; -fn no_tag_position_to_tag_position(position: u64) -> u64 { - let cur_chunk = position / CHUNK_SIZE; - let cur_chunk_pos = position % CHUNK_SIZE; - cur_chunk * CHUNK_TAG_SIZE + cur_chunk_pos + /// Internal `Read`::read but returning a mla `Error` + /// /!\ This method does not check the tag of each decrypted block + fn read_internal_unauthenticated(&mut self, buf: &mut [u8]) -> Result { + let cache_to_consume = CHUNK_SIZE - self.chunk_cache.position(); + if cache_to_consume == 0 { + // Cache totally consumed, renew it + self.current_chunk_number += 1; + if self.load_in_cache_unauthenticated()?.is_none() { + // No more byte in the inner layer + return Ok(0); + } + return self.read_internal_unauthenticated(buf); + } + // Consume at most the bytes leaving in the cache, to detect the renewal need + let size = std::cmp::min(cache_to_consume as usize, buf.len()); + self.chunk_cache + .read(&mut buf[..size]) + .map_err(|e| e.into()) + } } -fn tag_position_to_no_tag_position(position: u64) -> u64 { - // Assume the position is not inside a tag. If so, round to the end of the - // current chunk - let cur_chunk = position / CHUNK_TAG_SIZE; - let cur_chunk_pos = position % CHUNK_TAG_SIZE; - cur_chunk * CHUNK_SIZE + std::cmp::min(cur_chunk_pos, CHUNK_SIZE) +impl Read for EncryptionLayerInternal { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.read_internal(buf).map_err(|e| e.into()) + } } -impl<'a, R: 'a + Read + Seek> Seek for EncryptionLayerReader<'a, R> { +impl Seek for EncryptionLayerInternal { fn seek(&mut self, pos: SeekFrom) -> io::Result { // `pos` is the position without considering tags match pos { @@ -455,74 +509,118 @@ impl<'a, R: 'a + Read + Seek> Seek for EncryptionLayerReader<'a, R> { } } +pub struct EncryptionLayerReader<'a, R: Read + Seek>( + EncryptionLayerInternal>, +); + +impl<'a, R: Read + Seek> EncryptionLayerReader<'a, R> { + pub fn new( + inner: Box>, + config: &EncryptionReaderConfig, + ) -> Result { + Ok(EncryptionLayerReader(EncryptionLayerInternal::new( + inner, config, + )?)) + } +} + +impl<'a, R: 'a + InnerReaderTrait> LayerReader<'a, R> for EncryptionLayerReader<'a, R> { + fn into_inner(self) -> Option>> { + Some(self.0.inner) + } + + fn into_raw(self: Box) -> R { + self.0.inner.into_raw() + } + + fn initialize(&mut self) -> Result<(), Error> { + // Recursive call + self.0.inner.initialize()?; + + // Load the current buffer in cache + self.rewind()?; + Ok(()) + } +} + +impl<'a, R: 'a + Read + Seek> Read for EncryptionLayerReader<'a, R> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.0.read(buf) + } +} + +// Returns how many chunk are present at position `position` +const CHUNK_TAG_SIZE: u64 = CHUNK_SIZE + TAG_LENGTH as u64; + +fn no_tag_position_to_tag_position(position: u64) -> u64 { + let cur_chunk = position / CHUNK_SIZE; + let cur_chunk_pos = position % CHUNK_SIZE; + cur_chunk * CHUNK_TAG_SIZE + cur_chunk_pos +} + +fn tag_position_to_no_tag_position(position: u64) -> u64 { + // Assume the position is not inside a tag. If so, round to the end of the + // current chunk + let cur_chunk = position / CHUNK_TAG_SIZE; + let cur_chunk_pos = position % CHUNK_TAG_SIZE; + cur_chunk * CHUNK_SIZE + std::cmp::min(cur_chunk_pos, CHUNK_SIZE) +} + +impl<'a, R: 'a + Read + Seek> Seek for EncryptionLayerReader<'a, R> { + fn seek(&mut self, pos: SeekFrom) -> io::Result { + self.0.seek(pos) + } +} + // ---------- Fail-Safe Reader ---------- pub struct EncryptionLayerFailSafeReader<'a, R: Read> { - inner: Box>, - cipher: AesGcm256, - key: Key, - nonce: [u8; NONCE_SIZE], - current_chunk_number: u32, - current_chunk_offset: u64, + internal: EncryptionLayerInternal>, + decryption_mode: FailSafeReaderDecryptionMode, } -impl<'a, R: 'a + Read> EncryptionLayerFailSafeReader<'a, R> { +impl<'a, R: Read> EncryptionLayerFailSafeReader<'a, R> { pub fn new( inner: Box>, config: &EncryptionReaderConfig, ) -> Result { - match config.encrypt_parameters { - Some((key, nonce)) => Ok(Self { - inner, - cipher: AesGcm256::new(&key, &build_nonce(nonce, 0), b"")?, - key, - nonce, - current_chunk_number: 0, - current_chunk_offset: 0, - }), - None => Err(Error::PrivateKeyNeeded), - } + let mut layer = Self { + internal: EncryptionLayerInternal::new(inner, config)?, + decryption_mode: config.failsafe_mode, + }; + layer.internal.load_in_cache_unauthenticated()?; + Ok(layer) } } impl<'a, R: 'a + Read> LayerFailSafeReader<'a, R> for EncryptionLayerFailSafeReader<'a, R> { fn into_inner(self) -> Option>> { - Some(self.inner) + Some(self.internal.inner) } fn into_raw(self: Box) -> R { - self.inner.into_raw() + self.internal.inner.into_raw() } } impl<'a, R: Read> Read for EncryptionLayerFailSafeReader<'a, R> { + /// Behavior changes depending on config.FailSafeReaderDecryptionMode + /// - OnlyAuthenticatedData: only authenticated data is returned + /// - DataEvenUnauthenticated: all data is returned, even if not authenticated fn read(&mut self, buf: &mut [u8]) -> io::Result { - if self.current_chunk_offset == CHUNK_SIZE { - // Ignore the tag and renew the cipher - io::copy( - &mut (&mut self.inner).take(TAG_LENGTH as u64), - &mut io::sink(), - )?; - self.current_chunk_number += 1; - self.current_chunk_offset = 0; - self.cipher = AesGcm256::new( - &self.key, - &build_nonce(self.nonce, self.current_chunk_number), - b"", - )?; - return self.read(buf); + match self.decryption_mode { + FailSafeReaderDecryptionMode::OnlyAuthenticatedData => { + // catch AuthenticatedDecryptionWrongTag to gracefully stop the reading + match self.internal.read_internal(buf) { + Ok(size) => Ok(size), + Err(Error::AuthenticatedDecryptionWrongTag) => Ok(0), + Err(e) => Err(e.into()), + } + } + FailSafeReaderDecryptionMode::DataEvenUnauthenticated => { + Ok(self.internal.read_internal_unauthenticated(buf)?) + } } - - // AesGcm256 is working in place, so we use a temporary buffer - let mut buf_tmp = [0u8; CIPHER_BUF_SIZE as usize]; - let size = std::cmp::min(CIPHER_BUF_SIZE as usize, buf.len()); - // Read at most the chunk size, to detect when renewal is needed - let size = std::cmp::min((CHUNK_SIZE - self.current_chunk_offset) as usize, size); - let len = self.inner.read(&mut buf_tmp[..size])?; - self.current_chunk_offset += len as u64; - self.cipher.decrypt_unauthenticated(&mut buf_tmp[..len]); - (&buf_tmp[..len]).read_exact(&mut buf[..len])?; - Ok(len) } } @@ -573,6 +671,7 @@ mod tests { let config = EncryptionReaderConfig { private_keys: Vec::new(), encrypt_parameters: Some((KEY, NONCE)), + failsafe_mode: FailSafeReaderDecryptionMode::OnlyAuthenticatedData, }; let mut encrypt_r = EncryptionLayerReader::new(Box::new(RawLayerReader::new(buf)), &config).unwrap(); @@ -590,6 +689,7 @@ mod tests { let config = EncryptionReaderConfig { private_keys: Vec::new(), encrypt_parameters: Some((KEY, NONCE)), + failsafe_mode: FailSafeReaderDecryptionMode::OnlyAuthenticatedData, }; let mut encrypt_r = EncryptionLayerFailSafeReader::new( Box::new(RawLayerFailSafeReader::new(out.as_slice())), @@ -613,6 +713,7 @@ mod tests { let config = EncryptionReaderConfig { private_keys: Vec::new(), encrypt_parameters: Some((KEY, NONCE)), + failsafe_mode: FailSafeReaderDecryptionMode::OnlyAuthenticatedData, }; let mut encrypt_r = EncryptionLayerFailSafeReader::new( Box::new(RawLayerFailSafeReader::new(&out[..stop])), @@ -621,10 +722,77 @@ mod tests { .unwrap(); let mut output = Vec::new(); encrypt_r.read_to_end(&mut output).unwrap(); + // Thanks to the encrypt layer construction, we can recover `stop` bytes assert_eq!(output.as_slice(), &FAKE_FILE[..stop]); } + #[test] + fn failsafe_auth_vs_unauth() { + // Prepare inner layer (to be encrypted) + let file = Vec::new(); + let mut encrypt_w = Box::new( + EncryptionLayerWriter::new( + Box::new(RawLayerWriter::new(file)), + &EncryptionConfig { + ecc_keys: Vec::new(), + key: KEY, + nonce: NONCE, + }, + ) + .unwrap(), + ); + + // Write a 2*CHUNK_SIZE + 128 bytes stream + let length = (2 * CHUNK_SIZE + 128) as usize; + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0); + let data: Vec = Alphanumeric.sample_iter(&mut rng).take(length).collect(); + encrypt_w.write_all(&data).unwrap(); + encrypt_w.finalize().unwrap(); + let out = encrypt_w.into_raw(); + assert_eq!(out.len(), length + 3 * TAG_LENGTH); + + // data: [CHUNK1 (CHUNK_SIZE)][TAG1][CHUNK2 (CHUNK_SIZE)][TAG2][CHUNK3 (128)][TAG3] + // Truncate to remove the last tag + let trunc = &out[..out.len() - TAG_LENGTH]; + + // Failsafe read with tag checking + let config = EncryptionReaderConfig { + private_keys: Vec::new(), + encrypt_parameters: Some((KEY, NONCE)), + failsafe_mode: FailSafeReaderDecryptionMode::OnlyAuthenticatedData, + }; + let mut encrypt_r = EncryptionLayerFailSafeReader::new( + Box::new(RawLayerFailSafeReader::new(trunc)), + &config, + ) + .unwrap(); + let mut output_authent = Vec::new(); + encrypt_r.read_to_end(&mut output_authent).unwrap(); + + // We should have correctly read 2*CHUNK_SIZE inner data, the last 128 bytes being unauthenticated + assert_eq!(output_authent.len(), 2 * CHUNK_SIZE as usize); + assert_eq!(output_authent, data[..output_authent.len()]); + + // Failsafe read without tag checking + let config = EncryptionReaderConfig { + private_keys: Vec::new(), + encrypt_parameters: Some((KEY, NONCE)), + failsafe_mode: FailSafeReaderDecryptionMode::DataEvenUnauthenticated, + }; + let mut encrypt_r = EncryptionLayerFailSafeReader::new( + Box::new(RawLayerFailSafeReader::new(trunc)), + &config, + ) + .unwrap(); + let mut output_unauthent = Vec::new(); + encrypt_r.read_to_end(&mut output_unauthent).unwrap(); + + // We should have correctly read 2*CHUNK_SIZE + 128 bytes, the last 128 bytes being unauthenticated + assert_eq!(output_unauthent.len(), length); + assert_eq!(output_unauthent, data); + } + #[test] fn seek_encrypt() { // First, encrypt a dummy file @@ -636,6 +804,7 @@ mod tests { let config = EncryptionReaderConfig { private_keys: Vec::new(), encrypt_parameters: Some((KEY, NONCE)), + failsafe_mode: FailSafeReaderDecryptionMode::OnlyAuthenticatedData, }; let mut encrypt_r = EncryptionLayerReader::new(Box::new(RawLayerReader::new(buf)), &config).unwrap(); @@ -689,6 +858,7 @@ mod tests { let config = EncryptionReaderConfig { private_keys: Vec::new(), encrypt_parameters: Some((KEY, NONCE)), + failsafe_mode: FailSafeReaderDecryptionMode::OnlyAuthenticatedData, }; let mut encrypt_r = EncryptionLayerReader::new(Box::new(RawLayerReader::new(buf)), &config).unwrap(); diff --git a/mlar/src/main.rs b/mlar/src/main.rs index cf0d138d..00f057ac 100644 --- a/mlar/src/main.rs +++ b/mlar/src/main.rs @@ -279,13 +279,19 @@ fn open_mla_file<'a>(matches: &ArgMatches) -> Result, Ml fn open_failsafe_mla_file<'a>( matches: &ArgMatches, ) -> Result, MlarError> { - let config = readerconfig_from_matches(matches); + let mut config = readerconfig_from_matches(matches); // Safe to use unwrap() because the option is required() let mla_file = matches.get_one::("input").unwrap(); let path = Path::new(&mla_file); let file = File::open(path)?; + // Handle authenticated/unauthenticated data + if matches.get_flag("allow_unauthenticated_data") { + eprintln!("[WARNING] Some of the data might be unauthenticated, use it at your own risk"); + config.failsafe_return_data_even_unauthenticated(); + } + // Instantiate reader Ok(ArchiveFailSafeReader::from_config(file, config)?) } @@ -1161,6 +1167,13 @@ fn app() -> clap::Command { .subcommand( Command::new("repair") .about("Try to repair a MLA Archive into a fresh MLA Archive") + .arg( + Arg::new("allow_unauthenticated_data") + .long("allow-unauthenticated-data") + .help("Allow extraction of unauthenticated data from the archive. USE THIS OPTION ONLY IF NECESSARY") + .action(ArgAction::SetTrue) + .required(false), + ) .args(&input_args) .args(&output_args), ) diff --git a/mlar/tests/integration.rs b/mlar/tests/integration.rs index 740ef636..6aab24d7 100644 --- a/mlar/tests/integration.rs +++ b/mlar/tests/integration.rs @@ -427,6 +427,129 @@ fn test_truncated_repair_list_tar() { assert_eq!(fname2content.len(), 0); } +#[test] +fn test_repair_auth_unauth() { + let mlar_file = NamedTempFile::new("output.mla").unwrap(); + let mlar_repaired_file = NamedTempFile::new("repaired.mla").unwrap(); + let ecc_public = Path::new("../samples/test_x25519_pub.pem"); + let ecc_private = Path::new("../samples/test_x25519.pem"); + + // Create files + let testfs = setup(); + + // `mlar create -o output.mla -l encrypt -p samples/test_x25519_pub.pem file1.bin` + let mut cmd = Command::cargo_bin(UTIL).unwrap(); + cmd.arg("create") + .arg("-o") + .arg(mlar_file.path()) + .arg("-l") + .arg("encrypt") + .arg("-p") + .arg(ecc_public) + .arg(testfs.files[0].path()); + + let file_list = format!("{}\n", testfs.files[0].path().to_string_lossy()); + + println!("{cmd:?}"); + let assert = cmd.assert(); + assert.success().stderr(String::from(&file_list)); + + // `mlar list -i output.mla -k samples/test_x25519.pem` + let mut cmd = Command::cargo_bin(UTIL).unwrap(); + cmd.arg("list") + .arg("-i") + .arg(mlar_file.path()) + .arg("-k") + .arg(ecc_private); + + println!("{cmd:?}"); + let assert = cmd.assert(); + assert.success().stdout(file_list.clone()); + + // Truncate output.mla + let mut data = Vec::new(); + File::open(mlar_file.path()) + .unwrap() + .read_to_end(&mut data) + .unwrap(); + File::create(mlar_file.path()) + .unwrap() + .write_all(&data[..data.len() * 6 / 7]) + .unwrap(); + + // `mlar repair -i output.mla -k samples/test_x25519.pem -p samples/test_x25519_pub.pem -o repaired.mla -l encrypt` + let mut cmd = Command::cargo_bin(UTIL).unwrap(); + cmd.arg("repair") + .arg("-i") + .arg(mlar_file.path()) + .arg("-k") + .arg(ecc_private) + .arg("-p") + .arg(ecc_public) + .arg("-o") + .arg(mlar_repaired_file.path()) + .arg("-l") + .arg("encrypt"); + + println!("{cmd:?}"); + let assert = cmd.assert(); + assert.success(); + + // `mlar cat -i repaired.mla -k samples/test_x25519.pem file1.bin` + let mut cmd = Command::cargo_bin(UTIL).unwrap(); + cmd.arg("cat") + .arg("-i") + .arg(mlar_repaired_file.path()) + .arg("-k") + .arg(ecc_private) + .arg(testfs.files[0].path()); + + println!("{cmd:?}"); + let assert = cmd.assert(); + let output_auth = assert.get_output(); + + // `mlar repair --allow-unauthenticated-data -i output.mla -k samples/test_x25519.pem -p samples/test_x25519_pub.pem -o repaired.mla -l encrypt` + let mut cmd = Command::cargo_bin(UTIL).unwrap(); + cmd.arg("repair") + .arg("--allow-unauthenticated-data") + .arg("-i") + .arg(mlar_file.path()) + .arg("-k") + .arg(ecc_private) + .arg("-p") + .arg(ecc_public) + .arg("-o") + .arg(mlar_repaired_file.path()) + .arg("-l") + .arg("encrypt"); + + println!("{cmd:?}"); + let assert = cmd.assert(); + assert.success(); + + // `mlar cat -i repaired.mla -k samples/test_x25519.pem file1.bin` + let mut cmd = Command::cargo_bin(UTIL).unwrap(); + cmd.arg("cat") + .arg("-i") + .arg(mlar_repaired_file.path()) + .arg("-k") + .arg(ecc_private) + .arg(testfs.files[0].path()); + + println!("{cmd:?}"); + let assert = cmd.assert(); + let output_unauth = assert.get_output(); + + // Output unauthenticated must be longer than the authenticated one + assert!(output_unauth.stdout.len() > output_auth.stdout.len()); + + // Data must be the same + assert_eq!( + output_auth.stdout, + output_unauth.stdout[..output_auth.stdout.len()] + ); +} + #[test] fn test_multiple_keys() { // Key parsing is common for each subcommands, so test only one: `list`