When you protect either your workbook or one of your worksheets with a password in Excel, Excel internally generates a 16-bit hash of your password and stores it instead of the original password text. The hashing algorithm used for that was previously unknown, but thanks to the infamous Office Open XML specification it is now documented for the world to see (take a look at *Part 4, Section 3.3.1.81 – Sheet Protection Options* for the details). Thankfully, the algorithm is identical for all recent versions of Excel including XP, 2003 and 2007, so you can simply reuse the documented algorithm for the older versions of Excel too.

But alas! the documented algorithm is incorrect; it does not produce correct hash values. Being determined to find out the correct algorithm, however, I started to analyze the hashes that the documented algorithm produces, and compare them with the real hash values that Excel generates, in order to decipher the correct algorithm.

In the end, the documented algorithm was, although not accurate, pretty close enough that I was able to make a few changes and derive the algorithm that generates correct values. The following code:

#include <stdio.h> using namespace std; typedef unsigned char sal_uInt8; typedef unsigned short sal_uInt16; sal_uInt16 getPasswordHash(const char* szPassword) { sal_uInt16 cchPassword = strlen(szPassword); sal_uInt16 wPasswordHash = 0; if (!cchPassword) return wPasswordHash; const char* pch = &szPassword[cchPassword]; while (pch-- != szPassword) { wPasswordHash = ((wPasswordHash >> 14) & 0x01) | ((wPasswordHash << 1) & 0x7fff); wPasswordHash ^= *pch; } wPasswordHash = ((wPasswordHash >> 14) & 0x01) | ((wPasswordHash << 1) & 0x7fff); wPasswordHash ^= (0x8000 | ('N' << 8) | 'K'); wPasswordHash ^= cchPassword; return wPasswordHash; } int main (int argc, char** argv) { if (argc < 2) exit(1); printf("input password = %s\n", argv[1]); sal_uInt16 hash = getPasswordHash(argv[1]); printf("hash = %4.4X\n", hash); return 0; } |

produces the right hash value from an arbitrary password. One caveat: this algorithm takes an 8-bit char array, so if the input value consists of 16-bit unicode characters, it needs to be first converted into 8-bit character array. The conversion algorithm is also documented in the OOXML specification. I have not tested it yet, but I hope that algorithm is correct. ;-)