Is there any way to know whether a doc
/ppt
/xls
file is password-protected even before opening the file?
Detecting a password-protected document
Asked Answered
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;
}
}
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. Thanks –
Alexander
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 trick –
Ait
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
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.
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.