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
11pub struct EwfPassword {
16 bytes: Zeroizing<Vec<u8>>,
17}
18
19impl EwfPassword {
20 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum EncryptionMethod {
50 XWaysAes128Ctr,
52 XWaysAes256Ctr,
54}
55
56#[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 #[must_use]
73 pub const fn method(self) -> EncryptionMethod {
74 self.method
75 }
76
77 #[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}