Skip to main content

ewf_image/
encryption.rs

1use std::fmt;
2
3use aes::{Aes128, Aes256};
4use ctr::cipher::{KeyIvInit, StreamCipher};
5use sha2::{Digest, Sha256};
6use subtle::ConstantTimeEq;
7use zeroize::Zeroizing;
8
9use crate::{EwfError, Result};
10
11/// Password bytes supplied for one encrypted EWF image open attempt.
12///
13/// The password owns its byte allocation and zeroizes that allocation when it
14/// is dropped. Its [`Debug`](fmt::Debug) representation is always redacted.
15pub struct EwfPassword {
16    bytes: Zeroizing<Vec<u8>>,
17}
18
19impl EwfPassword {
20    /// Takes ownership of raw password bytes without applying an encoding or
21    /// normalization step.
22    #[must_use]
23    pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Self {
24        Self {
25            bytes: Zeroizing::new(bytes.into()),
26        }
27    }
28
29    /// Copies a UTF-8 password into zeroizing owned storage.
30    #[must_use]
31    pub fn utf8(password: &str) -> Self {
32        Self::from_bytes(password.as_bytes().to_vec())
33    }
34
35    pub(crate) fn as_bytes(&self) -> &[u8] {
36        &self.bytes
37    }
38}
39
40impl fmt::Debug for EwfPassword {
41    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
42        let _secret_length = self.bytes.len();
43        formatter.write_str("EwfPassword([REDACTED])")
44    }
45}
46
47/// Encryption method applied to an opened EWF image.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum EncryptionMethod {
50    /// X-Ways AES-128 in big-endian counter mode.
51    XWaysAes128Ctr,
52    /// X-Ways AES-256 in little-endian counter mode.
53    XWaysAes256Ctr,
54}
55
56/// Non-secret encryption status for an opened EWF image.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct EncryptionInfo {
59    method: EncryptionMethod,
60    password_verifier_present: bool,
61}
62
63impl EncryptionInfo {
64    pub(crate) fn from_xways(metadata: &XWaysEncryptionMetadata) -> Self {
65        Self {
66            method: metadata.method,
67            password_verifier_present: metadata.password_verifier.is_some(),
68        }
69    }
70
71    /// Returns the image's encryption method.
72    #[must_use]
73    pub const fn method(self) -> EncryptionMethod {
74        self.method
75    }
76
77    /// Returns whether the image contained a password verification hash.
78    #[must_use]
79    pub const fn password_verifier_present(self) -> bool {
80        self.password_verifier_present
81    }
82}
83
84pub(crate) const XWAYS_ENCRYPTION_DATA_SIZE: usize = 84;
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub(crate) struct XWaysEncryptionMetadata {
88    pub(crate) method: EncryptionMethod,
89    pub(crate) flags: u16,
90    pub(crate) salt: [u8; 32],
91    pub(crate) initial_counter: [u8; 16],
92    pub(crate) password_verifier: Option<[u8; 32]>,
93}
94
95impl XWaysEncryptionMetadata {
96    pub(crate) fn parse(data: &[u8]) -> Result<Self> {
97        if data.len() != XWAYS_ENCRYPTION_DATA_SIZE {
98            return Err(EwfError::Malformed(format!(
99                "X-Ways EWF1 encryption section has size {}, expected {XWAYS_ENCRYPTION_DATA_SIZE}",
100                data.len()
101            )));
102        }
103
104        let raw_method = u16::from_le_bytes([data[0], data[1]]);
105        let flags = u16::from_le_bytes([data[2], data[3]]);
106        if raw_method >= 3 {
107            return Err(EwfError::Malformed(format!(
108                "invalid X-Ways EWF1 encryption method {raw_method}"
109            )));
110        }
111        if flags & 0x0fff >= 8 {
112            return Err(EwfError::Malformed(format!(
113                "invalid X-Ways EWF1 encryption flags 0x{flags:04x}"
114            )));
115        }
116        if (raw_method == 0 && flags & 1 == 0) || (raw_method == 1 && flags & 1 != 0) {
117            return Err(EwfError::Malformed(format!(
118                "X-Ways EWF1 encryption method {raw_method} conflicts with flags 0x{flags:04x}"
119            )));
120        }
121
122        let method = match raw_method {
123            0 => EncryptionMethod::XWaysAes128Ctr,
124            1 => EncryptionMethod::XWaysAes256Ctr,
125            2 => {
126                return Err(EwfError::Unsupported(
127                    "X-Ways EWF1 encryption method 2".into(),
128                ));
129            }
130            _ => unreachable!("raw method was validated above"),
131        };
132        let salt = data[4..36]
133            .try_into()
134            .expect("X-Ways encryption metadata length checked");
135        let initial_counter = data[36..52]
136            .try_into()
137            .expect("X-Ways encryption metadata length checked");
138        let password_verifier = (flags & 2 == 0).then(|| {
139            data[52..84]
140                .try_into()
141                .expect("X-Ways encryption metadata length checked")
142        });
143
144        Ok(Self {
145            method,
146            flags,
147            salt,
148            initial_counter,
149            password_verifier,
150        })
151    }
152}
153
154pub(crate) struct EncryptionContext {
155    method: EncryptionMethod,
156    key: DerivedKey,
157    initial_counter: [u8; 16],
158}
159
160enum DerivedKey {
161    Aes128(Zeroizing<[u8; 16]>),
162    Aes256(Zeroizing<[u8; 32]>),
163}
164
165impl fmt::Debug for EncryptionContext {
166    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
167        formatter
168            .debug_struct("EncryptionContext")
169            .field("method", &self.method)
170            .field("key", &"[REDACTED]")
171            .field("initial_counter", &"[REDACTED]")
172            .finish()
173    }
174}
175
176impl EncryptionContext {
177    pub(crate) fn derive(
178        metadata: &XWaysEncryptionMetadata,
179        password: &EwfPassword,
180    ) -> Result<Self> {
181        let password = xways_password_bytes(metadata.method, password.as_bytes())?;
182        if metadata.password_verifier.as_ref().is_some_and(|expected| {
183            let actual = xways_password_verifier(metadata.method, &password, &metadata.salt);
184            !bool::from(actual[..].ct_eq(expected))
185        }) {
186            return Err(EwfError::PasswordRejected);
187        }
188
189        let key = match metadata.method {
190            EncryptionMethod::XWaysAes128Ctr => {
191                DerivedKey::Aes128(derive_aes128_key(&password, &metadata.salt))
192            }
193            EncryptionMethod::XWaysAes256Ctr => {
194                DerivedKey::Aes256(derive_aes256_key(&password, &metadata.salt))
195            }
196        };
197        Ok(Self {
198            method: metadata.method,
199            key,
200            initial_counter: metadata.initial_counter,
201        })
202    }
203
204    pub(crate) fn apply_keystream(&self, stream_offset: u64, bytes: &mut [u8]) -> Result<()> {
205        if bytes.is_empty() {
206            return Ok(());
207        }
208        if !stream_offset.is_multiple_of(16) {
209            return Err(EwfError::Malformed(
210                "X-Ways AES chunk offset is not block aligned".into(),
211            ));
212        }
213
214        let block_index = stream_offset / 16;
215        let final_block_delta =
216            u64::try_from((bytes.len() - 1) / 16).map_err(|_| counter_overflow())?;
217        let counter = counter_at_chunk_offset(self.initial_counter, block_index)?;
218
219        match &self.key {
220            DerivedKey::Aes128(key) => {
221                u128::from_be_bytes(counter)
222                    .checked_add(u128::from(final_block_delta))
223                    .ok_or_else(counter_overflow)?;
224                type Aes128Ctr = ctr::Ctr128BE<Aes128>;
225                let mut cipher = Aes128Ctr::new((&**key).into(), (&counter).into());
226                cipher.apply_keystream(bytes);
227            }
228            DerivedKey::Aes256(key) => {
229                u64::from_le_bytes(counter[..8].try_into().expect("counter prefix length"))
230                    .checked_add(final_block_delta)
231                    .ok_or_else(counter_overflow)?;
232                type Aes256Ctr = ctr::Ctr64LE<Aes256>;
233                let mut cipher = Aes256Ctr::new((&**key).into(), (&counter).into());
234                cipher.apply_keystream(bytes);
235            }
236        }
237        Ok(())
238    }
239
240    #[cfg(test)]
241    fn from_test_aes128_key(key: [u8; 16], initial_counter: [u8; 16]) -> Self {
242        Self {
243            method: EncryptionMethod::XWaysAes128Ctr,
244            key: DerivedKey::Aes128(Zeroizing::new(key)),
245            initial_counter,
246        }
247    }
248}
249
250fn counter_at_chunk_offset(initial_counter: [u8; 16], block_index: u64) -> Result<[u8; 16]> {
251    let mut counter = initial_counter;
252    let prefix = u64::from_le_bytes(counter[..8].try_into().expect("counter prefix length"))
253        .checked_add(block_index)
254        .ok_or_else(counter_overflow)?;
255    counter[..8].copy_from_slice(&prefix.to_le_bytes());
256    Ok(counter)
257}
258
259fn counter_overflow() -> EwfError {
260    EwfError::Malformed("X-Ways AES counter overflow".into())
261}
262
263fn xways_password_bytes(method: EncryptionMethod, password: &[u8]) -> Result<Zeroizing<[u8; 32]>> {
264    let maximum_length = match method {
265        EncryptionMethod::XWaysAes128Ctr => 16,
266        EncryptionMethod::XWaysAes256Ctr => 32,
267    };
268    if password.len() > maximum_length {
269        return Err(EwfError::PasswordRejected);
270    }
271
272    let mut bytes = Zeroizing::new([0_u8; 32]);
273    bytes[..password.len()].copy_from_slice(password);
274    Ok(bytes)
275}
276
277fn derive_aes256_key(password: &[u8; 32], salt: &[u8; 32]) -> Zeroizing<[u8; 32]> {
278    Zeroizing::new(
279        Sha256::new()
280            .chain_update(password)
281            .chain_update(salt)
282            .finalize()
283            .into(),
284    )
285}
286
287fn derive_aes128_key(password: &[u8; 32], salt: &[u8; 32]) -> Zeroizing<[u8; 16]> {
288    let key = derive_aes256_key(password, salt);
289    let mut reduced = Zeroizing::new([0_u8; 16]);
290    for (output, (first, second)) in reduced.iter_mut().zip(key[..16].iter().zip(&key[16..])) {
291        *output = first ^ second;
292    }
293    reduced
294}
295
296fn xways_password_verifier(
297    method: EncryptionMethod,
298    password: &[u8; 32],
299    salt: &[u8; 32],
300) -> Zeroizing<[u8; 32]> {
301    let rounds = match method {
302        EncryptionMethod::XWaysAes128Ctr => 100_000,
303        EncryptionMethod::XWaysAes256Ctr => 1,
304    };
305    let mut previous: Zeroizing<[u8; 32]> = Zeroizing::new(Sha256::digest(password).into());
306    let mut current = Zeroizing::new([0_u8; 32]);
307    for round in 0..rounds {
308        *current = Sha256::new()
309            .chain_update(password)
310            .chain_update(previous.as_slice())
311            .finalize()
312            .into();
313        if round + 1 < rounds {
314            previous.copy_from_slice(current.as_slice());
315        }
316    }
317    Zeroizing::new(
318        Sha256::new()
319            .chain_update(salt)
320            .chain_update(previous.as_slice())
321            .chain_update(current.as_slice())
322            .finalize()
323            .into(),
324    )
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    const PASSWORD: &[u8] = b"xways-test";
332    const SALT: [u8; 32] = [
333        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
334        0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
335        0x1e, 0x1f,
336    ];
337
338    fn hex_vec(value: &str) -> Vec<u8> {
339        value
340            .as_bytes()
341            .as_chunks::<2>()
342            .0
343            .iter()
344            .map(|pair| {
345                let high = char::from(pair[0]).to_digit(16).expect("hex digit");
346                let low = char::from(pair[1]).to_digit(16).expect("hex digit");
347                u8::try_from((high << 4) | low).expect("hex byte")
348            })
349            .collect()
350    }
351
352    fn hex_array<const N: usize>(value: &str) -> [u8; N] {
353        hex_vec(value).try_into().expect("hex has expected length")
354    }
355
356    fn aes256_context(initial_counter: [u8; 16]) -> EncryptionContext {
357        let password = EwfPassword::from_bytes(PASSWORD.to_vec());
358        let metadata = XWaysEncryptionMetadata {
359            method: EncryptionMethod::XWaysAes256Ctr,
360            flags: 0x1000,
361            salt: SALT,
362            initial_counter,
363            password_verifier: None,
364        };
365        EncryptionContext::derive(&metadata, &password).unwrap()
366    }
367
368    #[test]
369    fn parses_authentic_xways_encryption_metadata() {
370        let aes128 = XWaysEncryptionMetadata::parse(&hex_vec(
371            "0000011061F995F4B65662828E420139572C452260666DCD481BBBCA8E98528E3F8D8BBA9342D4A3AE0E16F923C5A37B9389A83D14F51270CF749C0E82CA06C8A0ED97602A55ACF684A6A18FCC4F0021BCCC95E4",
372        ))
373        .unwrap();
374        assert_eq!(aes128.method, EncryptionMethod::XWaysAes128Ctr);
375        assert_eq!(aes128.flags, 0x1001);
376        assert_eq!(
377            aes128.salt,
378            hex_array("61F995F4B65662828E420139572C452260666DCD481BBBCA8E98528E3F8D8BBA")
379        );
380        assert_eq!(
381            aes128.initial_counter,
382            hex_array("9342D4A3AE0E16F923C5A37B9389A83D")
383        );
384        assert_eq!(
385            aes128.password_verifier,
386            Some(hex_array(
387                "14F51270CF749C0E82CA06C8A0ED97602A55ACF684A6A18FCC4F0021BCCC95E4"
388            ))
389        );
390
391        let aes256 = XWaysEncryptionMetadata::parse(&hex_vec(
392            "01000010FD1E9C28B616BF087EAB72E63578FA65998724AB118F5977485FD0CA970492724376F18ED9736D06E3AB9D3D80EA2DC0F54130378826D53F929D6AE526A1974CA63A10593D097535EE481DD29CA81163",
393        ))
394        .unwrap();
395        assert_eq!(aes256.method, EncryptionMethod::XWaysAes256Ctr);
396        assert_eq!(aes256.flags, 0x1000);
397        assert_eq!(
398            aes256.salt,
399            hex_array("FD1E9C28B616BF087EAB72E63578FA65998724AB118F5977485FD0CA97049272")
400        );
401        assert_eq!(
402            aes256.initial_counter,
403            hex_array("4376F18ED9736D06E3AB9D3D80EA2DC0")
404        );
405        assert_eq!(
406            aes256.password_verifier,
407            Some(hex_array(
408                "F54130378826D53F929D6AE526A1974CA63A10593D097535EE481DD29CA81163"
409            ))
410        );
411    }
412
413    #[test]
414    fn parses_verifier_absence_from_xways_flags() {
415        let mut data = [0_u8; 84];
416        data[..2].copy_from_slice(&1_u16.to_le_bytes());
417        data[2..4].copy_from_slice(&0x1002_u16.to_le_bytes());
418
419        let metadata = XWaysEncryptionMetadata::parse(&data).unwrap();
420
421        assert_eq!(metadata.password_verifier, None);
422    }
423
424    #[test]
425    fn rejects_invalid_xways_encryption_metadata() {
426        let error = XWaysEncryptionMetadata::parse(&[0_u8; 83]).unwrap_err();
427        assert!(matches!(error, EwfError::Malformed(_)));
428
429        for (method, flags) in [(3_u16, 0_u16), (0, 0), (1, 1), (0, 0x1009)] {
430            let mut data = [0_u8; 84];
431            data[..2].copy_from_slice(&method.to_le_bytes());
432            data[2..4].copy_from_slice(&flags.to_le_bytes());
433
434            let error = XWaysEncryptionMetadata::parse(&data).unwrap_err();
435            assert!(
436                matches!(error, EwfError::Malformed(_)),
437                "method={method}, flags=0x{flags:04x}, error={error:?}"
438            );
439        }
440    }
441
442    #[test]
443    fn derives_reverse_engineered_xways_keys() {
444        let password = xways_password_bytes(EncryptionMethod::XWaysAes256Ctr, PASSWORD).unwrap();
445        assert_eq!(
446            *derive_aes256_key(&password, &SALT),
447            hex_array("e6e743cc93230c187367b822f06a5207dbbb2f76ca9782ddf7387f1112f71c7c")
448        );
449        assert_eq!(
450            *derive_aes128_key(&password, &SALT),
451            hex_array("3d5c6cba59b48ec5845fc733e29d4e7b")
452        );
453    }
454
455    #[test]
456    fn computes_method_specific_xways_password_verifiers() {
457        let password = xways_password_bytes(EncryptionMethod::XWaysAes256Ctr, PASSWORD).unwrap();
458        assert_eq!(
459            *xways_password_verifier(EncryptionMethod::XWaysAes256Ctr, &password, &SALT),
460            hex_array("67848e0c6345512ce5fd75dd57d6ee5f94fcae9fefb5eddc85f147d706c6364b")
461        );
462        assert_eq!(
463            *xways_password_verifier(EncryptionMethod::XWaysAes128Ctr, &password, &SALT),
464            hex_array("bc2387cb286603d72cd6cc0b83c0a9fff7ff08f3ebefe57ad74464f3698abd2d")
465        );
466    }
467
468    #[test]
469    fn canonicalizes_xways_passwords_to_fixed_zero_padded_buffers() {
470        let password = xways_password_bytes(EncryptionMethod::XWaysAes128Ctr, b"abc").unwrap();
471        assert_eq!(&password[..3], b"abc");
472        assert!(password[3..].iter().all(|byte| *byte == 0));
473
474        assert!(matches!(
475            xways_password_bytes(EncryptionMethod::XWaysAes128Ctr, &[b'a'; 17]),
476            Err(EwfError::PasswordRejected)
477        ));
478        assert!(matches!(
479            xways_password_bytes(EncryptionMethod::XWaysAes256Ctr, &[b'a'; 33]),
480            Err(EwfError::PasswordRejected)
481        ));
482    }
483
484    #[test]
485    fn aes128_ctr_matches_nist_big_endian_vector() {
486        let mut data = hex_vec("6bc1bee22e409f96e93d7e117393172a");
487        let context = EncryptionContext::from_test_aes128_key(
488            hex_array("2b7e151628aed2a6abf7158809cf4f3c"),
489            hex_array("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"),
490        );
491
492        context.apply_keystream(0, &mut data).unwrap();
493
494        assert_eq!(data, hex_vec("874d6191b620e3261bef6864990db6ce"));
495    }
496
497    #[test]
498    fn aes256_ctr_increments_the_low_order_first_byte() {
499        let context = aes256_context(hex_array("000102030405060708090a0b0c0d0e0f"));
500        let mut two_blocks = vec![0_u8; 32];
501
502        context.apply_keystream(0, &mut two_blocks).unwrap();
503
504        assert_eq!(
505            &two_blocks[..16],
506            hex_vec("d6d49d46ee7b6965ca7cb74ee3923d5f")
507        );
508        assert_eq!(
509            &two_blocks[16..],
510            hex_vec("d980e413ca4e5da559db937f4712841c")
511        );
512    }
513
514    #[test]
515    fn aes128_chunk_offsets_advance_the_counter_prefix() {
516        let context = EncryptionContext::from_test_aes128_key(
517            hex_array("2b7e151628aed2a6abf7158809cf4f3c"),
518            hex_array("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"),
519        );
520        let mut data = [0_u8; 16];
521
522        context.apply_keystream(16, &mut data).unwrap();
523
524        assert_eq!(data, hex_array("eeb9afc6c9b7e3d53576f29fe1e17805"));
525    }
526
527    #[test]
528    fn rejects_unaligned_xways_chunk_offsets() {
529        let context = aes256_context(hex_array("000102030405060708090a0b0c0d0e0f"));
530        let mut data = [0_u8; 16];
531
532        let error = context.apply_keystream(1, &mut data).unwrap_err();
533
534        assert!(
535            matches!(error, EwfError::Malformed(message) if message == "X-Ways AES chunk offset is not block aligned")
536        );
537    }
538
539    #[test]
540    fn empty_ctr_read_at_maximum_counter_is_allowed() {
541        let context = EncryptionContext::from_test_aes128_key([0; 16], [0xff; 16]);
542        let mut empty = [];
543
544        context.apply_keystream(u64::MAX, &mut empty).unwrap();
545    }
546
547    #[test]
548    fn ctr_read_rejects_counter_wrap() {
549        let context = EncryptionContext::from_test_aes128_key([0; 16], [0xff; 16]);
550        let mut data = [0_u8; 1];
551
552        let error = context.apply_keystream(16, &mut data).unwrap_err();
553
554        assert!(
555            matches!(error, EwfError::Malformed(message) if message == "X-Ways AES counter overflow")
556        );
557    }
558}