Обнаружение защищенного паролем документа

Есть ли способ узнать, защищен ли файл doc/ppt/xls паролем еще до открытия файла?


person logeeks    schedule 25.04.2011    source источник


Ответы (2)


Я создал служебный метод, который пытается определить, защищен ли данный офисный документ паролем или нет. Вот список преимуществ:

  • поддерживает документы Word, Excel и PowerPoint, как устаревшие (doc, xls, ppt), так и новую версию OpenXml (docx, xlsx, pptx)
  • не зависит от COM или любой другой библиотеки
  • требуются только пространства имен System, System.IO и System.Text
  • довольно быстрое и надежное обнаружение (учитывает устаревшие форматы файлов .doc, .ppt и .xls)
  • низкое использование памяти (максимум 64 КБ)

Вот код, надеюсь кому-нибудь пригодится:

public static class MsOfficeHelper
{
    /// <summary>
    /// Detects if a given office document is protected by a password or not.
    /// Supported formats: Word, Excel and PowerPoint (both legacy and OpenXml).
    /// </summary>
    /// <param name="fileName">Path to an office document.</param>
    /// <returns>True if document is protected by a password, false otherwise.</returns>
    public static bool IsPasswordProtected(string fileName)
    {
        using (var stream = File.OpenRead(fileName))
            return IsPasswordProtected(stream);
    }

    /// <summary>
    /// Detects if a given office document is protected by a password or not.
    /// Supported formats: Word, Excel and PowerPoint (both legacy and OpenXml).
    /// </summary>
    /// <param name="stream">Office document stream.</param>
    /// <returns>True if document is protected by a password, false otherwise.</returns>
    public static bool IsPasswordProtected(Stream stream)
    {
        // minimum file size for office file is 4k
        if (stream.Length < 4096)
            return false;

        // read file header
        stream.Seek(0, SeekOrigin.Begin);
        var compObjHeader = new byte[0x20];
        ReadFromStream(stream, compObjHeader);

        // check if we have plain zip file
        if (compObjHeader[0] == 'P' && compObjHeader[1] == 'K')
        {
            // this is a plain OpenXml document (not encrypted)
            return false;
        }

        // check compound object magic bytes
        if (compObjHeader[0] != 0xD0 || compObjHeader[1] != 0xCF)
        {
            // unknown document format
            return false;
        }

        int sectionSizePower = compObjHeader[0x1E];
        if (sectionSizePower < 8 || sectionSizePower > 16)
        {
            // invalid section size
            return false;
        }
        int sectionSize = 2 << (sectionSizePower - 1);

        const int defaultScanLength = 32768;
        long scanLength = Math.Min(defaultScanLength, stream.Length);

        // read header part for scan
        stream.Seek(0, SeekOrigin.Begin);
        var header = new byte[scanLength];
        ReadFromStream(stream, header);

        // check if we detected password protection
        if (ScanForPassword(stream, header, sectionSize))
            return true;

        // if not, try to scan footer as well

        // read footer part for scan
        stream.Seek(-scanLength, SeekOrigin.End);
        var footer = new byte[scanLength];
        ReadFromStream(stream, footer);

        // finally return the result
        return ScanForPassword(stream, footer, sectionSize);
    }

    static void ReadFromStream(Stream stream, byte[] buffer)
    {
        int bytesRead, count = buffer.Length;
        while (count > 0 && (bytesRead = stream.Read(buffer, 0, count)) > 0)
            count -= bytesRead;
        if (count > 0) throw new EndOfStreamException();
    }

    static bool ScanForPassword(Stream stream, byte[] buffer, int sectionSize)
    {
        const string afterNamePadding = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";

        try
        {
            string bufferString = Encoding.ASCII.GetString(buffer, 0, buffer.Length);

            // try to detect password protection used in new OpenXml documents
            // by searching for "EncryptedPackage" or "EncryptedSummary" streams
            const string encryptedPackageName = "E\0n\0c\0r\0y\0p\0t\0e\0d\0P\0a\0c\0k\0a\0g\0e" + afterNamePadding;
            const string encryptedSummaryName = "E\0n\0c\0r\0y\0p\0t\0e\0d\0S\0u\0m\0m\0a\0r\0y" + afterNamePadding;
            if (bufferString.Contains(encryptedPackageName) ||
                bufferString.Contains(encryptedSummaryName))
                return true;

            // try to detect password protection for legacy Office documents
            const int coBaseOffset = 0x200;
            const int sectionIdOffset = 0x74;

            // check for Word header
            const string wordDocumentName = "W\0o\0r\0d\0D\0o\0c\0u\0m\0e\0n\0t" + afterNamePadding;
            int headerOffset = bufferString.IndexOf(wordDocumentName, StringComparison.InvariantCulture);
            int sectionId;
            if (headerOffset >= 0)
            {
                sectionId = BitConverter.ToInt32(buffer, headerOffset + sectionIdOffset);
                int sectionOffset = coBaseOffset + sectionId * sectionSize;
                const int fibScanSize = 0x10;
                if (sectionOffset < 0 || sectionOffset + fibScanSize > stream.Length)
                    return false; // invalid document
                var fibHeader = new byte[fibScanSize];
                stream.Seek(sectionOffset, SeekOrigin.Begin);
                ReadFromStream(stream, fibHeader);
                short properties = BitConverter.ToInt16(fibHeader, 0x0A);
                // check for fEncrypted FIB bit
                const short fEncryptedBit = 0x0100;
                return (properties & fEncryptedBit) == fEncryptedBit;
            }

            // check for Excel header
            const string workbookName = "W\0o\0r\0k\0b\0o\0o\0k" + afterNamePadding;
            headerOffset = bufferString.IndexOf(workbookName, StringComparison.InvariantCulture);
            if (headerOffset >= 0)
            {
                sectionId = BitConverter.ToInt32(buffer, headerOffset + sectionIdOffset);
                int sectionOffset = coBaseOffset + sectionId * sectionSize;
                const int streamScanSize = 0x100;
                if (sectionOffset < 0 || sectionOffset + streamScanSize > stream.Length)
                    return false; // invalid document
                var workbookStream = new byte[streamScanSize];
                stream.Seek(sectionOffset, SeekOrigin.Begin);
                ReadFromStream(stream, workbookStream);
                short record = BitConverter.ToInt16(workbookStream, 0);
                short recordSize = BitConverter.ToInt16(workbookStream, sizeof(short));
                const short bofMagic = 0x0809;
                const short eofMagic = 0x000A;
                const short filePassMagic = 0x002F;
                if (record != bofMagic)
                    return false; // invalid BOF
                // scan for FILEPASS record until the end of the buffer
                int offset = sizeof(short) * 2 + recordSize;
                int recordsLeft = 16; // simple infinite loop check just in case
                do
                {
                    record = BitConverter.ToInt16(workbookStream, offset);
                    if (record == filePassMagic)
                        return true;
                    recordSize = BitConverter.ToInt16(workbookStream, sizeof(short) + offset);
                    offset += sizeof(short) * 2 + recordSize;
                    recordsLeft--;
                } while (record != eofMagic && recordsLeft > 0);
            }

            // check for PowerPoint user header
            const string currentUserName = "C\0u\0r\0r\0e\0n\0t\0 \0U\0s\0e\0r" + afterNamePadding;
            headerOffset = bufferString.IndexOf(currentUserName, StringComparison.InvariantCulture);
            if (headerOffset >= 0)
            {
                sectionId = BitConverter.ToInt32(buffer, headerOffset + sectionIdOffset);
                int sectionOffset = coBaseOffset + sectionId * sectionSize;
                const int userAtomScanSize = 0x10;
                if (sectionOffset < 0 || sectionOffset + userAtomScanSize > stream.Length)
                    return false; // invalid document
                var userAtom = new byte[userAtomScanSize];
                stream.Seek(sectionOffset, SeekOrigin.Begin);
                ReadFromStream(stream, userAtom);
                const int headerTokenOffset = 0x0C;
                uint headerToken = BitConverter.ToUInt32(userAtom, headerTokenOffset);
                // check for headerToken
                const uint encryptedToken = 0xF3D1C4DF;
                return headerToken == encryptedToken;
            }
        }
        catch (Exception ex)
        {
            // BitConverter exceptions may be related to document format problems
            // so we just treat them as "password not detected" result
            if (ex is ArgumentException)
                return false;
            // respect all the rest exceptions
            throw;
        }

        return false;
    }
}
person Funbit    schedule 23.10.2014
comment
Там какая-то приятная спелеология! - person jschroedl; 21.11.2014
comment
Отличная работа! Подтверждено, что он работает в Word, Excel для форматов 2013 и 97. - person Martin Murphy; 06.09.2015
comment
Похоже, он не может правильно определить его для 97 версии Powerpoint. Если вы пытаетесь потерпеть неудачу, а не зависнуть. Я предлагаю передать пароль сразу после пути к файлу при открытии. @c:\path\to\file.ppt + ::BadPassword:: - person Martin Murphy; 06.09.2015
comment
Я не совсем понял ваше предложение о неправильном пароле. Не могли бы вы загрузить PPT-файл этого неудачного 97-го? Постараюсь исправить функцию. Спасибо - person Funbit; 08.09.2015
comment
Шикарный класс, спасибо! У меня работает с защищенными паролем doc/x, xlsx, pptx, НО НЕ с защищенными паролем xls, ppt. - person Ofer; 27.09.2016
comment
@Ofer Не за что. Существует множество различных версий XLS, поэтому есть вероятность, что некоторые из них не поддерживаются. Тем не менее, функция была протестирована на более чем 10000 различных файлов Excel с 99%-ным правильным обнаружением, по крайней мере. Было бы здорово, если бы вы могли поделиться своим файлом XLS. - person Funbit; 28.09.2016
comment
Я создал файлы doc, ppt и xls, используя «Сохранить как» с docx, pptx и xlsx соответственно. Может быть, это не поддерживается. Но большинство файлов в старом формате были созданы старыми приложениями Office, так что это не должно быть проблемой. Я бы с радостью поделился файлом, но как? - person Ofer; 29.09.2016
comment
потрясающая работа, но, как сказал Офер, это не работает для файлов ppt, сохраненных с помощью сохранения, как в более новом офисном костюме (офис 16). Похоже, что в приведенном выше методе ScanForPassword отсутствует раздел кода для проверки пароля PPT. - person TechnicalSmile; 13.01.2017
comment
@MartinMurphy Я обновил код для поддержки устаревших документов PPT, попробуйте, теперь должно работать (по крайней мере, с документами, сохраненными после Office 2002). - person Funbit; 04.12.2017
comment
@TechnicalSmile Я обновил код для поддержки устаревших документов PPT, попробуйте. - person Funbit; 04.12.2017
comment
@Ofer Я обновил код для поддержки устаревших документов PPT, попробуйте. - person Funbit; 04.12.2017
comment
@Funbit Большое спасибо за ваши усилия. Я уже работаю в другой компании и занимаюсь другими делами, но я уверен, что это будет полезно многим другим. - person Ofer; 05.12.2017
comment
@Funbit Я попробовал ваше решение для файла Word, оно всегда возвращает false, даже если документ Word защищен паролем. не могли бы вы помочь мне с этим. - person Manoj Ahuja; 12.06.2018
comment
@Funbit ваше решение работает, если пароль установлен для открытия документа или excel, но если пароль установлен только для изменения, он возвращает false. Не могли бы вы помочь мне определить, установлен ли пароль только для изменения. - person Manoj Ahuja; 12.06.2018
comment
@ManojAhuja Извините, я не уверен, как применяются пароли редактирования (целью этого кода было определить, можно ли открыть документ для просмотра). Вы можете проверить формат файла XLS (download.microsoft.com/download/1/A/9/), возможно, это просто другой тип записи или флаг для управления редактированием паролей. - person Funbit; 14.06.2018
comment
@Funbit: Написание тестов для этого сейчас. Если я сохраню excel как xls 5.0/95, это не сработает. А вот для xls 97-2003 работает. Также проверим, есть ли подвох - person Michael P; 10.12.2020
comment
Похоже, что это возвращает значение true, когда сам файл Excel не защищен паролем, но пользователь выбрал параметр «Защитить структуру книги», чтобы запретить добавление листов и т. д. Как мы можем сделать так, чтобы этот метод возвращал значение true только для полностью защищенных паролем файлов? - person Chuck Norris; 26.07.2021

Вот грубая версия детектора паролей, который я сделал. Не нужно открывать какие-либо объекты Office.

    public static bool IsPassworded(string file) {
        var bytes = File.ReadAllBytes(file);
        return IsPassworded(bytes);
        return false;
    }
    public static bool IsPassworded(byte[] bytes) {
        var prefix = Encoding.Default.GetString(bytes.Take(2).ToArray());
        if (prefix == "PK") {
            //ZIP and not password protected
            return false;
        }
        if (prefix == "ÐÏ") {
            //Office format.

            //Flagged with password
            if (bytes.Skip(0x20c).Take(1).ToArray()[0] == 0x2f) return true; //XLS 2003
            if (bytes.Skip(0x214).Take(1).ToArray()[0] == 0x2f) return true; //XLS 2005
            if (bytes.Skip(0x20B).Take(1).ToArray()[0] == 0x13) return true; //DOC 2005

            if (bytes.Length < 2000) return false; //Guessing false
            var start = Encoding.Default.GetString(bytes.Take(2000).ToArray()); //DOC/XLS 2007+
            start = start.Replace("\0", " ");
            if (start.Contains("E n c r y p t e d P a c k a g e")) return true;
            return false;
        }

        //Unknown.
        return false;
    }

Это может быть не 100%. Флаги я нашел, сравнив несколько документов Excel и Word с паролем и без него. Чтобы добавить для PowerPoint, сделайте то же самое.

person Wolf5    schedule 15.01.2013
comment
здорово. это работает только для офисных документов? как насчет PDF? - person echo; 15.04.2016
comment
Приведенный выше код предназначен только для офисных документов (Microsoft). PDF-файлы — это продукт Adobe, и у них, вероятно, есть другой способ сделать это. Но может быть так же просто, как сравнить документ PDF до и после того, как он был защищен паролем, чтобы найти флаг (позицию), указывающий, что он защищен паролем. Затем просто создайте код, который реагирует на значение в этом месте. - person Wolf5; 16.04.2016