15

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

Robert Harvey
  • 178,213
  • 47
  • 333
  • 501
logeeks
  • 4,849
  • 15
  • 62
  • 93

2 Answers2

20

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;
    }
}
Funbit
  • 1,591
  • 2
  • 14
  • 15
  • 1
    That's some nice spelunking there! – jschroedl Nov 21 '14 at 15:46
  • Great work! Confirmed that it works on word, excel for both 2013 & 97 formats. – Martin Murphy Sep 06 '15 at 16:55
  • 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::" – Martin Murphy Sep 06 '15 at 18:34
  • 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. Thanks – Funbit Sep 08 '15 at 01:27
  • Awesome class, thanks! Works for me with password-protected doc/x, xlsx, pptx, BUT NOT with password-protected xls, ppt. – Ofer Sep 27 '16 at 09:39
  • @Ofer 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. – Funbit Sep 28 '16 at 12:14
  • 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? – Ofer Sep 29 '16 at 17:37
  • 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. – TechnicalSmile Jan 13 '17 at 11:56
  • @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). – Funbit Dec 04 '17 at 03:55
  • @TechnicalSmile I have updated the code to support legacy PPT documents, please try it. – Funbit Dec 04 '17 at 03:56
  • 1
    @Ofer I have updated the code to support legacy PPT documents, please try it. – Funbit Dec 04 '17 at 03:56
  • @Funbit 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. – Ofer Dec 05 '17 at 18:13
  • @Funbit 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. – Manoj Ahuja Jun 12 '18 at 14:17
  • @Funbit 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. – Manoj Ahuja Jun 12 '18 at 14:25
  • @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 (http://download.microsoft.com/download/1/A/9/1A96F918-793B-4A55-8B36-84113F275ADD/Excel97-2007BinaryFileFormat(xls)Specification.pdf), probably there is just another record type or a flag to control editing passwords. – Funbit Jun 14 '18 at 00:56
  • @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 trick – Michael P Dec 10 '20 at 18:27
  • 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? – Chuck Norris Jul 26 '21 at 20:55
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.

Wolf5
  • 16,600
  • 12
  • 59
  • 58
  • great. does this only work for office documents? how about PDFs? – echo Apr 15 '16 at 18:37
  • 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. – Wolf5 Apr 16 '16 at 12:32