【问题标题】:Detecting a password-protected document检测受密码保护的文档
【发布时间】:2017-12-21 16:14:23
【问题描述】:

有没有办法知道doc/ppt/xls 文件在打开文件之前是否受密码保护?

【问题讨论】:

    标签: c# .net vsto office-interop


    【解决方案1】:

    我创建了一个实用程序方法来尝试检测给定的办公文档是否受密码保护。以下是优点列表:

    • 支持 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;
        }
    }
    

    【讨论】:

    • 那是一些不错的探索!
    • 干得好!确认它适用于 2013 和 97 格式的 word、excel。
    • 看起来它无法正确检测到 97 个版本的 Powerpoint。如果你试图失败而不是挂起。我建议在打开文件路径之后立即传递密码,就像这样。 @"c:\path\to\file.ppt" + "::BadPassword::"
    • 我不太明白您关于密码错误的建议。能否请您上传那个失败的97的PPT文件?我将尝试修复该功能。谢谢
    • @Ofer 我已经更新代码支持旧版PPT文档,请尝试一下。
    【解决方案2】:

    这是我制作的密码检测器的粗略版本。不需要打开任何 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 添加,只需执行相同操作即可。

    【讨论】:

    • 太棒了。这仅适用于办公文件吗? PDF 怎么样?
    • 以上代码仅适用于办公文档(微软)。 PDF 是 Adob​​e 产品,他们可能有不同的方法来做到这一点。但它可能就像比较一个 PDF 文档在其被加密之前和之后以找到表明它被加密的标志(位置)一样简单。然后只需创建一个对该位置的值作出反应的代码。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-11-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多