Detecting a password-protected document
Asked Answered
G

2

15

Is there any way to know whether a doc/ppt/xls file is password-protected even before opening the file?

Gouty answered 25/4, 2011 at 11:55 Comment(0)
A
22

I have created an utility method that tries to detect if a given office document is protected by a password or not. Here are the list of advantages:

  • supports Word, Excel and PowerPoint documents, both legacy (doc, xls, ppt) and new OpenXml version (docx, xlsx, pptx)
  • does not depend on COM or any other library
  • requires only System, System.IO and System.Text namespaces
  • pretty fast and reliable detection (respects legacy .doc, .ppt and .xls file formats)
  • low memory usage (maximum 64KB)

Here is the code, hope someone will find it useful:

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;
    }
}
Alexander answered 23/10, 2014 at 6:0 Comment(17)
That's some nice spelunking there!Classy
Great work! Confirmed that it works on word, excel for both 2013 & 97 formats.Kimball
Looks like it fails to detect it properly for 97 versions of Powerpoint. If you're trying to fail rather than hang. I suggest passing the password immediately following the filepath on open like so. @"c:\path\to\file.ppt" + "::BadPassword::"Kimball
I didn't quite understand your suggestion about bad password. Could you please upload that failed 97's PPT file? I will try to fix the function. ThanksAlexander
Awesome class, thanks! Works for me with password-protected doc/x, xlsx, pptx, BUT NOT with password-protected xls, ppt.Ebenezer
@Ebenezer You're welcome. There are plenty of different XLS versions, so there is a chance that some of them aren't supported. However, the function were tested on more than 10000 different Excel files with 99% correct detection at least.. Would be great if you can share your XLS file.Alexander
I created doc, ppt, and xls files using "save as" with a docx, pptx, and xlsx, respectively. May be that isn't supported. But most of the files in the old format were created by old Office apps, so it shouldn't be a real problem. I would happily share the file, but how?Ebenezer
fabulous work but similar to what Ofer said, this is not working for ppt files saved using save as in newer office suit (office 16). It seems code section for checking password of PPT is missing in ScanForPassword method above.Vacuum
@MartinMurphy I have updated the code to support legacy PPT documents, please try it, should work now (at least with documents saved after Office 2002).Alexander
@Vacuum I have updated the code to support legacy PPT documents, please try it.Alexander
@Ebenezer I have updated the code to support legacy PPT documents, please try it.Alexander
@Alexander Thanks a lot for your efforts. I'm already in another company doing other things, but I'm sure it would be helpful to many others.Ebenezer
@Alexander I tried your solution for word file it's always returning false even if word document is password protected. can you please help me with these.Enigmatic
@Alexander your solution is working if password is set for open document or excel but if password is set for only modify then it's returning false. Can you please help me how to detect if password is set only for modify.Enigmatic
@ManojAhuja I'm sorry, I'm not sure how editing passwords are applied (the purpose of this code was to detect if document can be opened for viewing). You could check the XLS file format (download.microsoft.com/download/1/A/9/…), probably there is just another record type or a flag to control editing passwords.Alexander
@Funbit: Writing Tests for this now. If I save an excel as xls 5.0/95 it does not work. But for xls 97-2003 it works. Will also investigate if there is a trickAit
This seems to return true when the Excel file itself is not password-protected, but user has selected "Protect workbook structure" option to not allow adding sheets, etc. How can we make this method to return true only for fully password-protected files?Delmardelmer
C
10

Here is a crude version of a password detecter i made. Does not need to open any Office objects.

    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;
    }

It might not be 100%. The flags I found by comparing several Excel and Word documents with and without password. To add for PowerPoint just do the same.

Clingy answered 15/1, 2013 at 11:1 Comment(2)
great. does this only work for office documents? how about PDFs?Devisor
The above code is only for office documents (Microsoft). PDFs are an Adobe product and they probably have a different way to do it. But it might be as easy as to compare a PDF document before and after its been passworded to find a flag (position) that indicated it being passworded. Then just create a code that reacts to the value on that location.Clingy

© 2022 - 2024 — McMap. All rights reserved.