office_crypto/
lib.rs

1//! Pure Rust library to decrypt password-protected MS Office files.
2//!
3//! ## Example
4//!
5//! This crate exposes two functions: [`decrypt_from_file`] and [`decrypt_from_bytes`], which do exactly what they say they do. The resulting bytes can then be interpreted by any MS Office parser like [docx-rs](https://crates.io/crates/docx-rs) or [calamine](https://crates.io/crates/calamine).
6//!
7//! ```rust
8//! use docx_rs::read_docx;
9//! use office_crypto::decrypt_from_file;
10//!
11//! let path = "protected.docx";
12//! # let path = "tests/files/testStandard.docx";
13//! let decrypted = decrypt_from_file(path, "Password1234_").unwrap();
14//! let docx = read_docx(&decrypted).unwrap();
15//! ```
16//!
17//! ## Formats
18//!
19//! * [x] ECMA-376 (Agile Encryption/Standard Encryption)
20//!     * [x] MS-DOCX (OOXML) (Word 2007-Present)
21//!     * [x] MS-XLSX (OOXML) (Excel 2007-Present)
22//!     * [x] MS-PPTX (OOXML) (PowerPoint 2007-Present)
23//! * [~] Office Binary Document RC4 CryptoAPI
24//!     * [x] MS-DOC (Word 2002, 2003, 2004)
25//!     * [ ] MS-XLS (Excel 2002, 2003, 2004)
26//!     * [ ] MS-PPT (PowerPoint 2002, 2003, 2004)
27//! * [ ] ECMA-376 (Extensible Encryption)
28//!
29//! Agile encrypted files that use non-SHA512 hash functions will yield [`DecryptError::Unimplemented`], though I haven't yet encountered such a file.
30//!
31//! Note that the latest version of Word will create an Agile encrypted document.
32
33mod crypto;
34mod format;
35mod method;
36mod ole;
37
38use crypto::{AgileEncryptionInfo, StandardEncryptionInfo};
39use format::doc97;
40use ole::OleFile;
41use std::path::Path;
42use thiserror::Error;
43
44macro_rules! validate {
45    ($assert:expr, $err:expr) => {{
46        if ($assert) {
47            Ok(())
48        } else {
49            let error_code: DecryptError = $err;
50            Err(error_code)
51        }
52    }};
53}
54
55pub(crate) use validate;
56
57/// Open and decrypt an MS Office file, returning the decrypted bytes.
58///
59/// Returns [`DecryptError`] if any part of the file is malformed.
60pub fn decrypt_from_file<P: AsRef<Path>>(path: P, password: &str) -> Result<Vec<u8>, DecryptError> {
61    let mut olefile = OleFile::from_file(path)?;
62    olefile.init()?;
63
64    decrypt(&mut olefile, password)
65}
66
67/// Decrypt bytes as an MS Office file, returning the decrypted bytes.
68///
69/// Returns [`DecryptError`] if any part of the file is malformed.
70pub fn decrypt_from_bytes(raw: Vec<u8>, password: &str) -> Result<Vec<u8>, DecryptError> {
71    let mut olefile = OleFile::new(raw)?;
72    olefile.init()?;
73
74    decrypt(&mut olefile, password)
75}
76
77fn decrypt(olefile: &mut OleFile, password: &str) -> Result<Vec<u8>, DecryptError> {
78    // Detect format. OOXML has EncryptionInfo stream, binary formats don't
79    if olefile.exists(&["EncryptionInfo".to_owned()])? {
80        decrypt_ooxml(olefile, password)
81    } else if olefile.exists(&["WordDocument".to_owned()])? {
82        doc97::decrypt_doc97(olefile, password)
83    } else if olefile.exists(&["Workbook".to_owned()])? {
84        Err(DecryptError::Unimplemented(
85            "Excel binary format (.xls) not yet supported".to_owned(),
86        ))
87    } else if olefile.exists(&["Current User".to_owned()])? {
88        Err(DecryptError::Unimplemented(
89            "PowerPoint binary format (.ppt) not yet supported".to_owned(),
90        ))
91    } else {
92        Err(DecryptError::InvalidStructure)
93    }
94}
95
96fn decrypt_ooxml(olefile: &mut OleFile, password: &str) -> Result<Vec<u8>, DecryptError> {
97    let encryption_info_stream = olefile.open_stream(&["EncryptionInfo".to_owned()])?;
98    let encrypted_package_stream = olefile.open_stream(&["EncryptedPackage".to_owned()])?;
99
100    match encryption_info_stream.stream.get(..4) {
101        Some([4, 0, 4, 0]) => {
102            let aei = AgileEncryptionInfo::new(&encryption_info_stream)?;
103            let secret_key = aei.key_from_password(password)?;
104
105            aei.decrypt(&secret_key, &encrypted_package_stream)
106        }
107        Some([2 | 3 | 4, 0, 2, 0]) => {
108            let sei = StandardEncryptionInfo::new(&encryption_info_stream)?;
109            let secret_key = sei.key_from_password(password)?;
110
111            sei.decrypt(&secret_key, &encrypted_package_stream)
112        }
113        _ => Err(DecryptError::InvalidStructure),
114    }
115}
116
117#[derive(Error, Debug)]
118pub enum DecryptError {
119    #[error("IO Error")]
120    IoError(std::io::Error),
121    #[error("Invalid Olefile Header")]
122    InvalidHeader,
123    #[error("Invalid File Structure")]
124    InvalidStructure,
125    #[error("File is not encrypted")]
126    NotEncrypted,
127    #[error("Unimplemented: `{0}`")]
128    Unimplemented(String),
129    #[error("Unknown Error")]
130    Unknown,
131}