Откриване на документ, защитен с парола

Има ли някакъв начин да разберете дали файл 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)
  • ниско използване на паметта (максимум 64KB)

Ето кода, надявам се някой да го намери за полезен:

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 версии, така че има вероятност някои от тях да не се поддържат. Функцията обаче беше тествана на повече от 10 000 различни Excel файла с най-малко 99% правилно откриване. Би било чудесно, ако можете да споделите своя XLS файл. - person Funbit; 28.09.2016
comment
Създадох doc, ppt и xls файлове, използвайки съответно save as с docx, pptx и xlsx. Може това да не се поддържа. Но повечето от файловете в стария формат са създадени от стари приложения на Office, така че не би трябвало да е истински проблем. С удоволствие бих споделил файла, но как? - person Ofer; 29.09.2016
comment
невероятна работа, но подобно на това, което каза Офер, това не работи за ppt файлове, записани чрез запазване като в по-нов офис костюм (офис 16). Изглежда, че кодовата секция за проверка на паролата на PPT липсва в метода ScanForPassword по-горе. - 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