use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
#[cfg(feature = "dmg-encrypted")]
use std::io::{self, Write};
use crate::Result;
#[cfg(feature = "dmg-encrypted")]
use crate::block::BlockDevice;
pub const ENCRCDSA_MAGIC: &[u8; 8] = b"encrcdsa";
pub const ENCRCDSA_V2_HEADER_MIN_BYTES: usize = 0xD8;
pub fn probe(path: &Path) -> Result<bool> {
let mut f = match File::open(path) {
Ok(f) => f,
Err(_) => return Ok(false),
};
let mut head = [0u8; 8];
if f.read_exact(&mut head).is_err() {
return Ok(false);
}
Ok(&head == ENCRCDSA_MAGIC)
}
#[derive(Debug, Clone)]
pub struct EncryptedDmgHeader {
pub version: u32,
pub enc_iv_size: u32,
pub encryption_mode: u32,
pub encryption_algorithm: u32,
pub pbkdf2_prng_algorithm: u32,
pub pbkdf2_iteration_count: u32,
pub pbkdf2_salt_length: u32,
pub pbkdf2_salt: [u8; 32],
pub blob_enc_iv_size: u32,
pub blob_enc_iv: [u8; 32],
pub blob_enc_key_bits: u32,
pub blob_enc_algorithm: u32,
pub blob_enc_padding: u32,
pub blob_enc_mode: u32,
pub encrypted_keyblob_size: u32,
pub encrypted_keyblob: Vec<u8>,
pub block_size: u32,
pub n_chunks: u64,
pub data_offset: u64,
pub data_size: u64,
}
impl EncryptedDmgHeader {
pub fn decode(buf: &[u8]) -> Result<Self> {
if buf.len() < ENCRCDSA_V2_HEADER_MIN_BYTES {
return Err(crate::Error::InvalidImage(format!(
"encrcdsa: header slice shorter than {ENCRCDSA_V2_HEADER_MIN_BYTES} bytes"
)));
}
if &buf[0..8] != ENCRCDSA_MAGIC {
return Err(crate::Error::InvalidImage(
"encrcdsa: magic mismatch (expected \"encrcdsa\")".into(),
));
}
let version = u32::from_be_bytes(buf[0x08..0x0C].try_into().unwrap());
if version != 2 {
return Err(crate::Error::Unsupported(format!(
"encrcdsa: version {version} not supported (only v2)"
)));
}
let enc_iv_size = u32::from_be_bytes(buf[0x0C..0x10].try_into().unwrap());
let encryption_mode = u32::from_be_bytes(buf[0x10..0x14].try_into().unwrap());
let encryption_algorithm = u32::from_be_bytes(buf[0x14..0x18].try_into().unwrap());
let pbkdf2_prng_algorithm = u32::from_be_bytes(buf[0x18..0x1C].try_into().unwrap());
let pbkdf2_iteration_count = u32::from_be_bytes(buf[0x1C..0x20].try_into().unwrap());
let pbkdf2_salt_length = u32::from_be_bytes(buf[0x20..0x24].try_into().unwrap());
let mut pbkdf2_salt = [0u8; 32];
pbkdf2_salt.copy_from_slice(&buf[0x24..0x44]);
let blob_enc_iv_size = u32::from_be_bytes(buf[0x44..0x48].try_into().unwrap());
let mut blob_enc_iv = [0u8; 32];
blob_enc_iv.copy_from_slice(&buf[0x48..0x68]);
let blob_enc_key_bits = u32::from_be_bytes(buf[0x68..0x6C].try_into().unwrap());
let blob_enc_algorithm = u32::from_be_bytes(buf[0x6C..0x70].try_into().unwrap());
let blob_enc_padding = u32::from_be_bytes(buf[0x70..0x74].try_into().unwrap());
let blob_enc_mode = u32::from_be_bytes(buf[0x74..0x78].try_into().unwrap());
let encrypted_keyblob_size = u32::from_be_bytes(buf[0x78..0x7C].try_into().unwrap());
let blob_start = 0x7C;
let blob_end = blob_start + encrypted_keyblob_size as usize;
if blob_end > buf.len() {
return Err(crate::Error::InvalidImage(format!(
"encrcdsa: keyblob ({encrypted_keyblob_size} bytes) overruns provided header buffer"
)));
}
let encrypted_keyblob = buf[blob_start..blob_end].to_vec();
let block_size = u32::from_be_bytes(buf[0xBC..0xC0].try_into().unwrap());
let n_chunks = u64::from_be_bytes(buf[0xC0..0xC8].try_into().unwrap());
let data_offset = u64::from_be_bytes(buf[0xC8..0xD0].try_into().unwrap());
let data_size = u64::from_be_bytes(buf[0xD0..0xD8].try_into().unwrap());
Ok(Self {
version,
enc_iv_size,
encryption_mode,
encryption_algorithm,
pbkdf2_prng_algorithm,
pbkdf2_iteration_count,
pbkdf2_salt_length,
pbkdf2_salt,
blob_enc_iv_size,
blob_enc_iv,
blob_enc_key_bits,
blob_enc_algorithm,
blob_enc_padding,
blob_enc_mode,
encrypted_keyblob_size,
encrypted_keyblob,
block_size,
n_chunks,
data_offset,
data_size,
})
}
pub fn salt(&self) -> &[u8] {
&self.pbkdf2_salt[..self.pbkdf2_salt_length as usize]
}
pub fn blob_iv(&self) -> &[u8] {
&self.blob_enc_iv[..self.blob_enc_iv_size as usize]
}
pub fn aes_key_len(&self) -> Result<usize> {
match self.encryption_mode {
0 => Ok(16),
1 => Ok(32),
other => Err(crate::Error::Unsupported(format!(
"encrcdsa: unknown encryption_mode {other} (expected 0 = AES-128, 1 = AES-256)"
))),
}
}
}
pub fn read_header(file: &mut File) -> Result<EncryptedDmgHeader> {
let mut buf = vec![0u8; ENCRCDSA_V2_HEADER_MIN_BYTES];
file.seek(SeekFrom::Start(0))?;
file.read_exact(&mut buf)?;
EncryptedDmgHeader::decode(&buf)
}
#[cfg(feature = "dmg-encrypted")]
#[derive(Debug)]
pub struct EncryptedDmgBackend {
file: File,
header: EncryptedDmgHeader,
aes_key: Vec<u8>,
hmac_key: [u8; 20],
virtual_size: u64,
cursor: u64,
}
#[cfg(feature = "dmg-encrypted")]
impl EncryptedDmgBackend {
pub fn open_with_password(path: &Path, password: &str) -> Result<Self> {
let mut file = File::open(path)?;
let header = read_header(&mut file)?;
if header.encryption_algorithm != 1 {
return Err(crate::Error::Unsupported(format!(
"encrcdsa: encryption_algorithm {} not supported (only 1 = AES_CBC)",
header.encryption_algorithm
)));
}
let aes_key_len = header.aes_key_len()?;
if header.blob_enc_algorithm != 3 {
return Err(crate::Error::Unsupported(format!(
"encrcdsa: blob_enc_algorithm {} not supported (only 3 = 3DES_EDE3_CBC)",
header.blob_enc_algorithm
)));
}
if header.blob_enc_key_bits != 192 {
return Err(crate::Error::Unsupported(format!(
"encrcdsa: blob_enc_key_bits {} not supported (only 192 = 3DES)",
header.blob_enc_key_bits
)));
}
if header.block_size == 0 {
return Err(crate::Error::InvalidImage(
"encrcdsa: block_size is zero".into(),
));
}
let mut kek = [0u8; 24];
pbkdf2::pbkdf2_hmac::<sha1::Sha1>(
password.as_bytes(),
header.salt(),
header.pbkdf2_iteration_count,
&mut kek,
);
let keyblob_plain = decrypt_keyblob(&kek, header.blob_iv(), &header.encrypted_keyblob)?;
let needed = aes_key_len + 20;
if keyblob_plain.len() < needed {
return Err(crate::Error::InvalidImage(format!(
"encrcdsa: unwrapped keyblob too short ({} bytes, need >= {})",
keyblob_plain.len(),
needed
)));
}
let aes_key = keyblob_plain[..aes_key_len].to_vec();
let mut hmac_key = [0u8; 20];
hmac_key.copy_from_slice(&keyblob_plain[aes_key_len..aes_key_len + 20]);
let virtual_size = header
.n_chunks
.checked_mul(header.block_size as u64)
.ok_or_else(|| {
crate::Error::InvalidImage("encrcdsa: n_chunks * block_size overflows u64".into())
})?;
Ok(Self {
file,
header,
aes_key,
hmac_key,
virtual_size,
cursor: 0,
})
}
pub fn header(&self) -> &EncryptedDmgHeader {
&self.header
}
fn decrypt_chunk(&mut self, chunk_index: u64) -> Result<Vec<u8>> {
let block_size = self.header.block_size as usize;
let abs_offset = self
.header
.data_offset
.checked_add(chunk_index * self.header.block_size as u64)
.ok_or_else(|| {
crate::Error::InvalidImage(
"encrcdsa: chunk absolute offset overflows the data fork".into(),
)
})?;
self.file.seek(SeekFrom::Start(abs_offset))?;
let mut ciphertext = vec![0u8; block_size];
self.file.read_exact(&mut ciphertext)?;
let iv = chunk_iv(&self.hmac_key, chunk_index as u32);
if ciphertext.len() % 16 != 0 {
return Err(crate::Error::InvalidImage(format!(
"encrcdsa: chunk ciphertext length {} is not a multiple of 16",
ciphertext.len()
)));
}
match self.aes_key.len() {
16 => decrypt_aes128_cbc(&self.aes_key, &iv, &mut ciphertext)?,
32 => decrypt_aes256_cbc(&self.aes_key, &iv, &mut ciphertext)?,
other => {
return Err(crate::Error::InvalidImage(format!(
"encrcdsa: unwrapped AES key has unexpected length {other}"
)));
}
}
Ok(ciphertext)
}
}
#[cfg(feature = "dmg-encrypted")]
fn decrypt_aes128_cbc(key: &[u8], iv: &[u8; 16], buf: &mut [u8]) -> Result<()> {
use cipher::{BlockDecryptMut, KeyIvInit, generic_array::GenericArray};
let mut dec = cbc::Decryptor::<aes::Aes128>::new_from_slices(key, iv).map_err(|e| {
crate::Error::InvalidImage(format!("encrcdsa: AES-128-CBC init failed: {e}"))
})?;
for block in buf.chunks_exact_mut(16) {
let g: &mut GenericArray<u8, _> = GenericArray::from_mut_slice(block);
dec.decrypt_block_mut(g);
}
Ok(())
}
#[cfg(feature = "dmg-encrypted")]
fn decrypt_aes256_cbc(key: &[u8], iv: &[u8; 16], buf: &mut [u8]) -> Result<()> {
use cipher::{BlockDecryptMut, KeyIvInit, generic_array::GenericArray};
let mut dec = cbc::Decryptor::<aes::Aes256>::new_from_slices(key, iv).map_err(|e| {
crate::Error::InvalidImage(format!("encrcdsa: AES-256-CBC init failed: {e}"))
})?;
for block in buf.chunks_exact_mut(16) {
let g: &mut GenericArray<u8, _> = GenericArray::from_mut_slice(block);
dec.decrypt_block_mut(g);
}
Ok(())
}
#[cfg(feature = "dmg-encrypted")]
fn decrypt_keyblob(kek: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>> {
use cipher::{BlockDecryptMut, KeyIvInit, block_padding::Pkcs7};
if kek.len() != 24 {
return Err(crate::Error::InvalidImage(format!(
"encrcdsa: KEK has wrong length {} (expected 24)",
kek.len()
)));
}
if iv.len() < 8 {
return Err(crate::Error::InvalidImage(format!(
"encrcdsa: 3DES IV slice too short ({} bytes)",
iv.len()
)));
}
let iv8: [u8; 8] = iv[..8].try_into().unwrap();
let dec = cbc::Decryptor::<des::TdesEde3>::new_from_slices(kek, &iv8)
.map_err(|e| crate::Error::InvalidImage(format!("encrcdsa: 3DES-CBC init failed: {e}")))?;
if ciphertext.len() % 8 != 0 || ciphertext.is_empty() {
return Err(crate::Error::InvalidImage(format!(
"encrcdsa: keyblob ciphertext length {} is not a positive multiple of 8",
ciphertext.len()
)));
}
let mut buf = ciphertext.to_vec();
let plain_slice = dec.decrypt_padded_mut::<Pkcs7>(&mut buf).map_err(|_| {
crate::Error::Unsupported(
"encrcdsa: keyblob unwrap failed — wrong password, or unsupported padding".into(),
)
})?;
let plain_len = plain_slice.len();
buf.truncate(plain_len);
Ok(buf)
}
#[cfg(feature = "dmg-encrypted")]
fn chunk_iv(hmac_key: &[u8; 20], chunk_index: u32) -> [u8; 16] {
use hmac::{Hmac, Mac};
let mut mac =
Hmac::<sha1::Sha1>::new_from_slice(hmac_key).expect("HMAC-SHA1 accepts any key length");
mac.update(&chunk_index.to_be_bytes());
let tag = mac.finalize().into_bytes();
let mut iv = [0u8; 16];
iv.copy_from_slice(&tag[..16]);
iv
}
#[cfg(feature = "dmg-encrypted")]
impl BlockDevice for EncryptedDmgBackend {
fn block_size(&self) -> u32 {
512
}
fn total_size(&self) -> u64 {
self.virtual_size
}
fn sync(&mut self) -> Result<()> {
Ok(())
}
fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<()> {
let size = self.virtual_size;
let end = offset
.checked_add(buf.len() as u64)
.ok_or(crate::Error::OutOfBounds {
offset,
len: buf.len() as u64,
size,
})?;
if end > size {
return Err(crate::Error::OutOfBounds {
offset,
len: buf.len() as u64,
size,
});
}
if buf.is_empty() {
return Ok(());
}
let bs = self.header.block_size as u64;
let mut filled = 0usize;
let mut cursor = offset;
while filled < buf.len() {
let chunk_index = cursor / bs;
let chunk_base = chunk_index * bs;
let plain = self.decrypt_chunk(chunk_index)?;
debug_assert_eq!(plain.len() as u64, bs);
let local_start = (cursor - chunk_base) as usize;
let available = (bs - (cursor - chunk_base)) as usize;
let want = (buf.len() - filled).min(available);
buf[filled..filled + want].copy_from_slice(&plain[local_start..local_start + want]);
filled += want;
cursor += want as u64;
}
Ok(())
}
fn write_at(&mut self, _offset: u64, _buf: &[u8]) -> Result<()> {
Err(crate::Error::Unsupported(
"encrcdsa: read-only container; writes are out of scope".into(),
))
}
}
#[cfg(feature = "dmg-encrypted")]
impl Read for EncryptedDmgBackend {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.cursor >= self.virtual_size {
return Ok(0);
}
let remaining = self.virtual_size - self.cursor;
let take = (buf.len() as u64).min(remaining) as usize;
if take == 0 {
return Ok(0);
}
self.read_at(self.cursor, &mut buf[..take])
.map_err(|e| io::Error::other(format!("{e}")))?;
self.cursor += take as u64;
Ok(take)
}
}
#[cfg(feature = "dmg-encrypted")]
impl Write for EncryptedDmgBackend {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
Err(io::Error::other("encrcdsa: read-only container"))
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[cfg(feature = "dmg-encrypted")]
impl Seek for EncryptedDmgBackend {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let total = self.virtual_size;
let new = match pos {
SeekFrom::Start(o) => o,
SeekFrom::Current(d) => (self.cursor as i64).saturating_add(d).max(0) as u64,
SeekFrom::End(d) => (total as i64).saturating_add(d).max(0) as u64,
};
self.cursor = new;
Ok(new)
}
}
#[cfg(not(feature = "dmg-encrypted"))]
#[derive(Debug)]
pub struct EncryptedDmgBackend {
_never: std::convert::Infallible,
}
#[cfg(not(feature = "dmg-encrypted"))]
impl EncryptedDmgBackend {
pub fn open_with_password(_path: &Path, _password: &str) -> Result<Self> {
Err(crate::Error::Unsupported(
"encrcdsa: encrypted DMG support requires the `dmg-encrypted` Cargo feature".into(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(clippy::too_many_arguments)]
fn build_header_bytes(
iter_count: u32,
salt: &[u8],
blob_iv: &[u8],
keyblob: &[u8],
encryption_mode: u32,
block_size: u32,
n_chunks: u64,
data_offset: u64,
) -> Vec<u8> {
let mut buf = vec![0u8; ENCRCDSA_V2_HEADER_MIN_BYTES];
buf[0..8].copy_from_slice(ENCRCDSA_MAGIC);
buf[0x08..0x0C].copy_from_slice(&2u32.to_be_bytes());
buf[0x0C..0x10].copy_from_slice(&32u32.to_be_bytes());
buf[0x10..0x14].copy_from_slice(&encryption_mode.to_be_bytes());
buf[0x14..0x18].copy_from_slice(&1u32.to_be_bytes()); buf[0x18..0x1C].copy_from_slice(&0u32.to_be_bytes()); buf[0x1C..0x20].copy_from_slice(&iter_count.to_be_bytes());
buf[0x20..0x24].copy_from_slice(&(salt.len() as u32).to_be_bytes());
buf[0x24..0x24 + salt.len()].copy_from_slice(salt);
buf[0x44..0x48].copy_from_slice(&(blob_iv.len() as u32).to_be_bytes());
buf[0x48..0x48 + blob_iv.len()].copy_from_slice(blob_iv);
buf[0x68..0x6C].copy_from_slice(&192u32.to_be_bytes());
buf[0x6C..0x70].copy_from_slice(&3u32.to_be_bytes()); buf[0x70..0x74].copy_from_slice(&7u32.to_be_bytes()); buf[0x74..0x78].copy_from_slice(&6u32.to_be_bytes()); buf[0x78..0x7C].copy_from_slice(&(keyblob.len() as u32).to_be_bytes());
let blob_start = 0x7C;
let blob_end = blob_start + keyblob.len();
if blob_end > 0xBC {
panic!("keyblob too large for fixed slot");
}
buf[blob_start..blob_end].copy_from_slice(keyblob);
buf[0xBC..0xC0].copy_from_slice(&block_size.to_be_bytes());
buf[0xC0..0xC8].copy_from_slice(&n_chunks.to_be_bytes());
buf[0xC8..0xD0].copy_from_slice(&data_offset.to_be_bytes());
buf[0xD0..0xD8].copy_from_slice(&(n_chunks * block_size as u64).to_be_bytes());
buf
}
#[test]
fn header_decodes_minimal_v2() {
let salt = b"saltsaltsaltsaltsalt"; let blob_iv = b"iv8iv8iv"; let keyblob = vec![0u8; 48];
let buf = build_header_bytes(1000, salt, blob_iv, &keyblob, 0, 4096, 4, 0x1000);
let h = EncryptedDmgHeader::decode(&buf).unwrap();
assert_eq!(h.version, 2);
assert_eq!(h.encryption_mode, 0);
assert_eq!(h.encryption_algorithm, 1);
assert_eq!(h.pbkdf2_iteration_count, 1000);
assert_eq!(h.salt(), salt);
assert_eq!(h.blob_iv(), blob_iv);
assert_eq!(h.block_size, 4096);
assert_eq!(h.n_chunks, 4);
assert_eq!(h.data_offset, 0x1000);
assert_eq!(h.aes_key_len().unwrap(), 16);
}
#[test]
fn header_rejects_wrong_magic() {
let mut buf =
build_header_bytes(1000, b"salt", b"iv8iv8iv", &[0u8; 48], 0, 4096, 1, 0x1000);
buf[0] = b'X';
let err = EncryptedDmgHeader::decode(&buf).unwrap_err();
match err {
crate::Error::InvalidImage(_) => {}
_ => panic!("expected InvalidImage, got {err:?}"),
}
}
#[test]
fn header_rejects_v1() {
let mut buf =
build_header_bytes(1000, b"salt", b"iv8iv8iv", &[0u8; 48], 0, 4096, 1, 0x1000);
buf[0x08..0x0C].copy_from_slice(&1u32.to_be_bytes());
let err = EncryptedDmgHeader::decode(&buf).unwrap_err();
match err {
crate::Error::Unsupported(_) => {}
_ => panic!("expected Unsupported, got {err:?}"),
}
}
#[test]
fn probe_recognises_v2_magic() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("enc.dmg");
let mut content = vec![0u8; 128];
content[..8].copy_from_slice(ENCRCDSA_MAGIC);
std::fs::write(&p, &content).unwrap();
assert!(probe(&p).unwrap());
}
#[test]
fn probe_misses_unrelated_file() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("not-encrypted.dmg");
std::fs::write(&p, b"random bytes").unwrap();
assert!(!probe(&p).unwrap());
}
#[cfg(not(feature = "dmg-encrypted"))]
#[test]
fn open_returns_unsupported_without_feature() {
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("enc.dmg");
std::fs::write(&p, ENCRCDSA_MAGIC).unwrap();
let err = EncryptedDmgBackend::open_with_password(&p, "irrelevant").unwrap_err();
match err {
crate::Error::Unsupported(_) => {}
_ => panic!("expected Unsupported, got {err:?}"),
}
}
#[cfg(feature = "dmg-encrypted")]
#[test]
fn round_trip_synthetic_aes128() {
use cipher::{BlockEncryptMut, KeyIvInit, block_padding::Pkcs7};
use hmac::{Hmac, Mac};
let password = "correct horse battery staple";
let iter_count: u32 = 100;
let salt: &[u8] = b"saltsaltsaltsaltsalt"; let blob_iv8: [u8; 8] = *b"ivivivIV";
let aes_key: [u8; 16] = *b"AESKEY-128-BIT!!";
let hmac_key: [u8; 20] = *b"HMACKEY-20-BYTES!!??";
let mut kek = [0u8; 24];
pbkdf2::pbkdf2_hmac::<sha1::Sha1>(password.as_bytes(), salt, iter_count, &mut kek);
let mut keyblob_plain = Vec::new();
keyblob_plain.extend_from_slice(&aes_key);
keyblob_plain.extend_from_slice(&hmac_key);
let mut keyblob_pad = keyblob_plain.clone();
let unpadded_len = keyblob_pad.len();
let pad_len = 8 - (unpadded_len % 8);
keyblob_pad.resize(unpadded_len + 16, 0); let enc = cbc::Encryptor::<des::TdesEde3>::new_from_slices(&kek, &blob_iv8).unwrap();
let ct = enc
.encrypt_padded_mut::<Pkcs7>(&mut keyblob_pad, unpadded_len)
.unwrap();
let keyblob_ciphertext = ct.to_vec();
assert_eq!(keyblob_ciphertext.len(), unpadded_len + pad_len);
let mut plaintext = vec![0u8; 4096];
for (i, b) in plaintext.iter_mut().enumerate() {
*b = ((i * 31 + 7) & 0xFF) as u8;
}
let mut mac = Hmac::<sha1::Sha1>::new_from_slice(&hmac_key).unwrap();
mac.update(&0u32.to_be_bytes());
let tag = mac.finalize().into_bytes();
let mut iv16 = [0u8; 16];
iv16.copy_from_slice(&tag[..16]);
let mut aes_enc = cbc::Encryptor::<aes::Aes128>::new_from_slices(&aes_key, &iv16).unwrap();
let mut chunk_ct = plaintext.clone();
for block in chunk_ct.chunks_exact_mut(16) {
let g = cipher::generic_array::GenericArray::from_mut_slice(block);
aes_enc.encrypt_block_mut(g);
}
let data_offset = 0xD8u64;
let mut file_bytes = build_header_bytes(
iter_count,
salt,
&blob_iv8,
&keyblob_ciphertext,
0,
4096,
1,
data_offset,
);
assert_eq!(file_bytes.len(), ENCRCDSA_V2_HEADER_MIN_BYTES);
file_bytes.extend_from_slice(&chunk_ct);
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("enc.dmg");
std::fs::write(&p, &file_bytes).unwrap();
let mut be = EncryptedDmgBackend::open_with_password(&p, password).unwrap();
assert_eq!(be.total_size(), 4096);
let mut out = vec![0u8; 4096];
be.read_at(0, &mut out).unwrap();
assert_eq!(out, plaintext);
let mut mid = vec![0u8; 16];
be.read_at(100, &mut mid).unwrap();
assert_eq!(mid, &plaintext[100..116]);
}
#[cfg(feature = "dmg-encrypted")]
#[test]
fn round_trip_synthetic_aes256() {
use cipher::{BlockEncryptMut, KeyIvInit, block_padding::Pkcs7};
use hmac::{Hmac, Mac};
let password = "another-password";
let iter_count: u32 = 64;
let salt: &[u8] = b"sodium_chloride_xx"; let blob_iv8: [u8; 8] = [0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88];
let aes_key: [u8; 32] = *b"AES256-KEY-MATERIAL-32-BYTES---!";
let hmac_key: [u8; 20] = *b"hmac-key-20-bytes-OK";
let mut kek = [0u8; 24];
pbkdf2::pbkdf2_hmac::<sha1::Sha1>(password.as_bytes(), salt, iter_count, &mut kek);
let mut keyblob_plain = Vec::new();
keyblob_plain.extend_from_slice(&aes_key);
keyblob_plain.extend_from_slice(&hmac_key);
let unpadded_len = keyblob_plain.len();
let mut keyblob_pad = keyblob_plain.clone();
keyblob_pad.resize(unpadded_len + 16, 0);
let enc = cbc::Encryptor::<des::TdesEde3>::new_from_slices(&kek, &blob_iv8).unwrap();
let ct = enc
.encrypt_padded_mut::<Pkcs7>(&mut keyblob_pad, unpadded_len)
.unwrap();
let keyblob_ciphertext = ct.to_vec();
let mut plain = vec![0u8; 8192];
for (i, b) in plain.iter_mut().enumerate() {
*b = ((i ^ (i >> 4)) & 0xFF) as u8;
}
let mut chunks_ct = Vec::with_capacity(8192);
for chunk_idx in 0u32..2 {
let mut mac = Hmac::<sha1::Sha1>::new_from_slice(&hmac_key).unwrap();
mac.update(&chunk_idx.to_be_bytes());
let tag = mac.finalize().into_bytes();
let mut iv16 = [0u8; 16];
iv16.copy_from_slice(&tag[..16]);
let mut aes_enc =
cbc::Encryptor::<aes::Aes256>::new_from_slices(&aes_key, &iv16).unwrap();
let start = (chunk_idx as usize) * 4096;
let mut buf = plain[start..start + 4096].to_vec();
for block in buf.chunks_exact_mut(16) {
let g = cipher::generic_array::GenericArray::from_mut_slice(block);
aes_enc.encrypt_block_mut(g);
}
chunks_ct.extend_from_slice(&buf);
}
let data_offset = 0xD8u64;
let mut file_bytes = build_header_bytes(
iter_count,
salt,
&blob_iv8,
&keyblob_ciphertext,
1, 4096,
2,
data_offset,
);
file_bytes.extend_from_slice(&chunks_ct);
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("enc256.dmg");
std::fs::write(&p, &file_bytes).unwrap();
let mut be = EncryptedDmgBackend::open_with_password(&p, password).unwrap();
assert_eq!(be.total_size(), 8192);
let mut out = vec![0u8; 8192];
be.read_at(0, &mut out).unwrap();
assert_eq!(out, plain);
let mut cross = vec![0u8; 64];
be.read_at(4096 - 32, &mut cross).unwrap();
assert_eq!(&cross[..32], &plain[4096 - 32..4096]);
assert_eq!(&cross[32..], &plain[4096..4096 + 32]);
}
#[cfg(feature = "dmg-encrypted")]
#[test]
fn wrong_password_rejected() {
use cipher::{BlockEncryptMut, KeyIvInit, block_padding::Pkcs7};
let password = "supersecret";
let iter_count: u32 = 100;
let salt: &[u8] = b"saltsaltsaltsaltsalt";
let blob_iv8: [u8; 8] = *b"ivivivIV";
let aes_key: [u8; 16] = *b"AESKEY-128-BIT!!";
let hmac_key: [u8; 20] = *b"HMACKEY-20-BYTES!!??";
let mut kek = [0u8; 24];
pbkdf2::pbkdf2_hmac::<sha1::Sha1>(password.as_bytes(), salt, iter_count, &mut kek);
let mut keyblob_plain = Vec::new();
keyblob_plain.extend_from_slice(&aes_key);
keyblob_plain.extend_from_slice(&hmac_key);
let unpadded_len = keyblob_plain.len();
let mut keyblob_pad = keyblob_plain.clone();
keyblob_pad.resize(unpadded_len + 16, 0);
let enc = cbc::Encryptor::<des::TdesEde3>::new_from_slices(&kek, &blob_iv8).unwrap();
let ct = enc
.encrypt_padded_mut::<Pkcs7>(&mut keyblob_pad, unpadded_len)
.unwrap();
let keyblob_ciphertext = ct.to_vec();
let data_offset = 0xD8u64;
let mut file_bytes = build_header_bytes(
iter_count,
salt,
&blob_iv8,
&keyblob_ciphertext,
0,
4096,
1,
data_offset,
);
file_bytes.extend_from_slice(&[0u8; 4096]);
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("enc.dmg");
std::fs::write(&p, &file_bytes).unwrap();
let err = EncryptedDmgBackend::open_with_password(&p, "wrong-password").unwrap_err();
match err {
crate::Error::Unsupported(msg) => {
assert!(msg.contains("wrong password") || msg.contains("padding"));
}
_ => panic!("expected Unsupported, got {err:?}"),
}
}
#[cfg(feature = "dmg-encrypted")]
#[test]
fn read_at_rejects_out_of_bounds() {
use cipher::{BlockEncryptMut, KeyIvInit, block_padding::Pkcs7};
let password = "pw";
let iter_count: u32 = 50;
let salt: &[u8] = b"saltsaltsaltsaltsalt";
let blob_iv8: [u8; 8] = *b"ivivivIV";
let aes_key: [u8; 16] = *b"AESKEY-128-BIT!!";
let hmac_key: [u8; 20] = *b"HMACKEY-20-BYTES!!??";
let mut kek = [0u8; 24];
pbkdf2::pbkdf2_hmac::<sha1::Sha1>(password.as_bytes(), salt, iter_count, &mut kek);
let mut keyblob_plain = Vec::new();
keyblob_plain.extend_from_slice(&aes_key);
keyblob_plain.extend_from_slice(&hmac_key);
let unpadded_len = keyblob_plain.len();
let mut keyblob_pad = keyblob_plain.clone();
keyblob_pad.resize(unpadded_len + 16, 0);
let enc = cbc::Encryptor::<des::TdesEde3>::new_from_slices(&kek, &blob_iv8).unwrap();
let ct = enc
.encrypt_padded_mut::<Pkcs7>(&mut keyblob_pad, unpadded_len)
.unwrap();
let keyblob_ciphertext = ct.to_vec();
let mut file_bytes = build_header_bytes(
iter_count,
salt,
&blob_iv8,
&keyblob_ciphertext,
0,
4096,
2,
0xD8,
);
file_bytes.extend_from_slice(&[0u8; 8192]);
let dir = tempfile::tempdir().unwrap();
let p = dir.path().join("enc.dmg");
std::fs::write(&p, &file_bytes).unwrap();
let mut be = EncryptedDmgBackend::open_with_password(&p, password).unwrap();
assert_eq!(be.total_size(), 8192);
let mut out = [0u8; 16];
let err = be.read_at(8192, &mut out).unwrap_err();
match err {
crate::Error::OutOfBounds { .. } => {}
_ => panic!("expected OutOfBounds, got {err:?}"),
}
}
}