Khalid Hamadeh
Research note · Cryptography

The Office password tables are a CRC

Microsoft Office’s old password code carries two tables of unexplained numbers, copied into LibreOffice, Apache POI and a dozen other projects. They are a standard 16-bit checksum, worked out in advance. A third table, inside Windows, is a 1990 journal table read in the wrong direction.

Khalid HamadehOctober 2026Code: github.com/khalid-hamadeh/office-password-crc

Type a password. Each letter switches on the entries of Microsoft’s table where its bits are 1, and the lit entries are XORed into a starting value from a second table. A textbook CRC-16 of the same letters gives the same 16-bit key.

Start value, InitialCode[6]0x1D0F
XOR of 0 lit entries0x0000
Key from Microsoft’s tables0x64CE
CRC-16 of the same letters0x64CE
The password box is drawn after Excel 97’s Protect Sheet dialog. XorMatrix (105 entries, in 15 rows of 7) and InitialCode, from [MS-OFFCRYPTO] §2.3.7.2, running on this page. Each row is one letter and its seven low bits; the last letter always uses the bottom row, and longer passwords use the rows above. The bottom-right entry, 1021, is the CRC-16-CCITT polynomial itself. Why this is a CRC

Summary

Question
Office’s oldest password protection, documented by Microsoft as “XOR obfuscation”, turns a password into a 16-bit key with two hard-coded tables of 105 and 15 numbers. The same numbers give half of Word’s legacy password hash in the Office Open XML standard. Neither document says where they come from.
Answer
Every XorMatrix entry is a power of x modulo the CRC-16-CCITT polynomial, 0x1021, and every InitialCode entry is 0xFFFF times a power of x. The key is the CRC-16/CCITT-FALSE checksum (also catalogued as CRC-16/IBM-3740) of the password, with the top bit of each byte cleared. This rebuilds both tables in full and matches every published example.
Second finding
The 256-byte table in Windows’ HashData function, as reimplemented in Wine, is Table I of Peter Pearson’s 1990 paper on hashing, with rows and columns swapped. The table itself, and its link to HashData, were already public.
How sure
Both results are exact computations. Why Microsoft shipped the CRC as a table is not documented. We found no earlier explanation, and neither result affects security.

1The question

Word and Excel files from the 1990s could be protected with a password in a way Microsoft’s own documentation calls “XOR obfuscation”. The password is turned into a 16-bit key, the key and password are mixed into a 16-byte array, and that array is XORed over the file. When Microsoft published its file-format specifications in 2008, the procedure appeared as pseudocode in [MS-OFFCRYPTO] §2.3.7.1 At its centre are two tables of 16-bit numbers:

From [MS-OFFCRYPTO] §2.3.7.2 (abridged; variable names shortened)
// 15 values
InitialCode ← (0xE1F0, 0x1D0F, 0xCC9C, …)
// 105 values
XorMatrix   ← (0xAEFC, 0x4DD9, 0x9BB2, …)

XorKey  ← InitialCode[length − 1]
Element ← 0x68
for each character, last to first:
  repeat 7 times:
    if character AND 0x40:
      XorKey ← XorKey XOR XorMatrix[Element]
    character ← character × 2
    Element   ← Element − 1

The same 120 numbers reappear in the Office Open XML standard, ECMA-376 Part 4 §14.8.1, as the “initial code array” and “encryption matrix” of the legacy password hash Word uses for document protection.2 Neither document explains them. They have since been copied into LibreOffice, Apache OpenOffice, Apache POI, NPOI, docx4j, PHPWord and others: 42 files in 36 public repositories, about 15 independent projects once vendored copies are merged. Many cite the specification as their source. None explains how the values were made.3

The same numbers, five projects
Point at a value to find it in the others
LibreOffice C++docpasswordhelper.cxx
pInitialCode[]
0xE1F00x1D0F0xCC9C0x84C0…
Apache POI JavaCryptoFunctions.java
INITIAL_CODE_ARRAY
0xE1F00x1D0F0xCC9C0x84C0…
INITIAL_CODE_ARRAY
0xE1F00x1D0F0xCC9C0x84C0…
$initialCodeArray
0xE1F00x1D0F0xCC9C0x84C0…
msoffcrypto-tool Pythonxor_obfuscation.py
initial_code
0xE1F00x1D0F0xCC9C0x84C0…
Figure 1. The first four InitialCode values as they appear today in five widely used open-source projects; each file path links to the line. All are copies, and none says how the values were made. About fifteen independent projects carry the tables.3

Microsoft did not defend it either. Introducing the specification in 2008, Microsoft engineer David LeBlanc called it “the worst way to protect a document” and wrote that its designers “did all sorts of silly things to make this hard to figure out”.4

The tables sit in an international standard and in the code of LibreOffice, OpenOffice and Apache POI, and for 18 years after the specification was published no one recorded what they are. They are a CRC-16 built on the same polynomial as the checksums in modems and SD cards, with that polynomial printed in the table itself.

2The answer

Read XorMatrix seven entries at a time, as MS-OFFCRYPTO prints it. Within each group, every entry is the one before it doubled, and whenever doubling overflows 16 bits the result is XORed with 0x1021:

Table 1. The first group of XorMatrix. Each entry is the one above it doubled, reduced by the CRC-16-CCITT polynomial on overflow.
EntryPrevious × 2Overflow?Published
0——0xAEFC
10x15DF8yes: drop bit 16, XOR 0x10210x4DD9
20x9BB2no0x9BB2
30x13764yes0x2745
40x4E8Ano0x4E8A
50x9D14no0x9D14
60x13A28yes0x2A09

0x1021 is the CRC-16-CCITT polynomial, x16 + x12 + x5 + 1, used in X.25, Bluetooth, XMODEM and SD cards. Doubling with that reduction is multiplication by x in the arithmetic CRCs are built on, and in that arithmetic every entry of both tables has a short closed form:

Algorithm 1. Both tables from one polynomial
P(x) = x16 + x12 + x5 + 1          // 0x1021

// r = 0…14, c = 0…6
XorMatrix[7r + c]  = x128 − 8r + c mod P(x)

// L = 1…15
InitialCode[L − 1] = 0xFFFF · x8L mod P(x)

Microsoft’s table even prints the polynomial: the last group of XorMatrix, the one every password’s final letter uses, begins with 0x1021 itself, which is x16 reduced once. Each earlier group starts eight powers higher, one byte’s worth of shifting, and holds seven entries because only the low seven bits of each character are used. InitialCode is the state of a CRC that starts at 0xFFFF after one zero byte per character.

So the procedure in Section 1 is a standard checksum. The XOR key is CRC-16/CCITT-FALSE of the password: polynomial 0x1021, initial value 0xFFFF, no bit reflection and no final XOR, run over the characters from first to last with the top bit of each cleared. The tables are that CRC unrolled into a lookup, with the characters taken last to first and the bits from the top. CRC catalogues list this variant as CRC-16/IBM-3740 as well as CRC-16/CCITT-FALSE.

The same key, bit by bit

Letters
Letter byte
Register
0x1021
Figure 2. A textbook CRC-16 run on the password from Figure 1. The register starts at FFFF; each letter is XORed into its top byte, then shifted out one bit at a time, and whenever a 1 falls off the left the polynomial 0x1021 is XORed in. Microsoft’s tables do the same arithmetic in advance.

3Verification

Algorithm 1 rebuilds all 105 entries of XorMatrix and all 15 of InitialCode exactly, from the polynomial and the start value 0xFFFF. The specification’s procedure and a textbook bit-by-bit CRC-16/CCITT-FALSE give the same key on 10,000 random passwords of printable characters and 10,000 of arbitrary bytes, from 1 to 15 long.

A 2008 post on Microsoft’s developer blogs by Eric White printed five sample outputs of code written from the Office Open XML specification, whose hash uses these tables for its high 16 bits; one example, “Example”, is also the worked example in ECMA-376 Part 4 §14.8.1.5 The standard first cuts the password to 15 characters and turns each into one byte: its low byte, or its high byte if the low byte is zero. A CRC of those bytes gives the published value in all five, including passwords in Cyrillic and Japanese:

Table 2. Published hashes (high 16 bits) and CRC-16/CCITT-FALSE of each password’s bytes, after the standard’s one-byte-per-character rule.
PasswordPublishedCRC
Example64CE64CE
фисD928D928
サインインC684C684
ĀbcCA21CA21
VeryVeryLongPassword (cut to 15)B5CCB5CC

For a given password length, both procedures are affine over GF(2): a start value XORed with one fixed contribution per set bit. So matching the table entries and start values already makes them the same function, and the random tests are a sanity check. A second implementation, written independently from the published tables and a one-paragraph statement of the claim, confirmed both closed forms and agreed on every one of more than 10,000 test passwords.a The script that accompanies this note checks everything above in under a second.

4A second table: Windows HashData

Windows exports a small function, HashData, that hashes a buffer to an output of up to 256 bytes.6 Wine’s reimplementation shows that it is a form of Pearson hashing, the method Peter Pearson of Lawrence Livermore National Laboratory published in Communications of the ACM in 1990: each byte is XORed into the hash and looked up in a fixed permutation of 0 to 255.7,8 That much was noticed before. A 2021 Stack Overflow answer called HashData “apparently a variant” of a Pearson-hash routine in a 1995 library archived at Rensselaer Polytechnic Institute (RPI).9

The table itself is Pearson’s, at least as Wine reproduces it; Wine’s test suite checks HashData’s output against Windows. His paper prints the permutation used in its tests as a 16 × 16 grid, Table I, “Pseudorandom Permutation of the Integers 0 through 255”. The HashData table is that grid read down the columns instead of across the rows, as Figure 3 shows.

Pearson’s Table I against Windows’ HashData table

16of 256 positions match Windows’ table
Same number as Windows’ tableDifferent numberDecimal, as Pearson printed them

This figure requires JavaScript. The reproduction script performs the same comparison.

Figure 3. Pearson’s Table I laid out in reading order and compared, position by position, with the HashData table in Wine’s source.

The paper means its grids to be read across the rows. Its Table II, a second permutation, is said to hash 31 common English words to 1 through 31 in alphabetical order. Read row by row it does, for all 31; read down the columns it gets none of them right.

Microsoft was not alone in the swap. The RPI routine the Stack Overflow answer pointed to, SymT_hash.c (revision dated 15 September 1995), carries the same transposed table, all 256 entries, under a comment saying it “uses the permutation printed with this article”.10 So the table and its link to HashData were already public. What had not been written down, as far as we found, is that it is Pearson’s Table I transposed. Whether Microsoft and the RPI author misread the paper independently or copied a common source is not known. How Pearson made his own permutation is not stated either; the paper says only that he experimented with randomly generated ones.

5The tables over the years

  1. 1990
    Peter Pearson publishes his hashing method in Communications of the ACM, with a 16 × 16 permutation as Table I.7
  2. 1995
    A library at RPI carries Pearson’s table, read down the columns, “the permutation printed with this article”.10
  3. Late 1990s
    Word and Excel protect documents with XOR obfuscation, its key built from XorMatrix and InitialCode.
  4. by 2000
    Windows ships HashData with the transposed Pearson table.6
  5. 2008
    Microsoft publishes the specifications. The tables appear in [MS-OFFCRYPTO] and ECMA-376 without explanation. David LeBlanc calls the scheme “the worst way to protect a document”.1,4
  6. 2008–26
    LibreOffice, Apache POI and about a dozen other projects reproduce the 120 numbers verbatim, without explanation.3
  7. 2021
    A Stack Overflow answer links HashData to the 1995 RPI routine.9

6Discussion

Established by computation: the two closed forms in Algorithm 1; that the specification’s key procedure equals CRC-16/CCITT-FALSE over the 7-bit password bytes; agreement with Microsoft’s published examples; the transposition relating HashData’s table to Pearson’s Table I; that row by row is the paper’s intended reading; and that the 1995 RPI routine carries the same transposed table.

Not established: why the CRC was shipped as a table rather than computed. LeBlanc’s remark about making the scheme hard to figure out suggests it was deliberate, but that is inference. A table of per-bit masks is also a fast way to compute a CRC, and the specification may just transcribe the code that shipped. Also not established: how Pearson generated his table, and whether Microsoft’s transposed copy and RPI’s share a source.

There is an irony in LeBlanc’s post. Of the other half of the scheme, the 16-bit password verifier, he wrote: “We’d have been much better off just taking a CRC16 of the password.”4 The key derivation already was a CRC-16.

Neither result affects security. XOR obfuscation has been documented as insecure since the specification was published, and HashData is not a security function.

The search for an earlier explanation covered the [MS-OFFCRYPTO] and ECMA-376 texts, the OpenOffice.org Excel file-format documentation, 108 messages on the Apache POI mailing lists, the commit histories of LibreOffice, Apache OpenOffice, Apache POI and Gnumeric, every public copy of the tables we found, Stack Overflow, Cryptography Stack Exchange, password-recovery vendors’ documentation and general web search. It included a 2007 Aspose blog post that Apache POI’s code cites next to these functions; the post is about Word 2007’s newer hash and mentions CRCs only as a general analogy.11 A separate, independent search in October 2026 also found none. A current open-source toolkit documents the scheme in detail and prints the tables, including the 0x1021 entry, without connecting them to a CRC.12 Not checked: the LibreOffice developer mailing-list archive, which cannot be searched directly. Corrections are welcome and will be credited here.

7A note on method

These results came from the same experiment as the earlier note on RC2’s π table: AI agents working through lookup tables that ship without an explanation, where a proposed answer can be checked exactly.13 The agent, Claude, pulled hard-coded tables from Wine and other open-source reimplementations of Microsoft software and tested each against known constructions. The CRC turned up in a test for entries that double under XOR; the HashData match came from comparing every collected table with every other, transposed and inverted. OpenAI’s Codex wrote the independent check in Section 3.

Notes

  1. a.The second implementation used its own CRC routine and confirmed the standard CRC-16/CCITT-FALSE check value, 0x29B1 for the string “123456789”.
  2. b.A smaller curiosity: the 15-byte padding array in the same section of [MS-OFFCRYPTO], BB FF FF BA FF FF B9 80 00 BE 0F 00 BF 0F 00, decodes as five 16-bit x86 instructions (mov bx,0FFFFh; mov dx,0FFFFh; mov cx,80h; mov si,0Fh; mov di,0Fh). The decoding is exact; that the bytes were lifted from code is a guess.

References

  1. [1]Microsoft. [MS-OFFCRYPTO]: Office Document Cryptography Structure, §2.3.7.1–2.3.7.4 (XOR obfuscation; CreateXorKey_Method1), version of 17 February 2026.
  2. [2]Ecma International. ECMA-376 Office Open XML File Formats, Part 4: Transitional Migration Features, 5th edition, December 2016, §14.8.1 “Legacy Password Hash Algorithm”.
  3. [3]Public copies of the tables located by searching for their first values: e.g. LibreOffice comphelper/source/misc/docpasswordhelper.cxx, Apache POI CryptoFunctions.java, NPOI, docx4j, PHPOffice, Xceed DocX, ExcelDataReader (42 files in 36 repositories, 18 October 2026); also msoffcrypto-tool.
  4. [4]D. LeBlanc. Office Crypto Follies. Microsoft (MSDN blogs archive), 3 July 2008.
  5. [5]E. White (code by B. McClellan). The Legacy Hashing Algorithm in Open XML. Microsoft (MSDN blogs archive), 22 February 2008.
  6. [6]Microsoft. HashData function (shlwapi.h); UrlHashA function.
  7. [7]P. K. Pearson. Fast Hashing of Variable-Length Text Strings. Communications of the ACM 33(6):677–680, June 1990. doi:10.1145/78973.78978.
  8. [8]Wine. dlls/kernelbase/path.c, hashdata_lookup and HashData (accessed 18 October 2026).
  9. [9]jimhark. Answer to “What hashing algorithm is HashData in Shlwapi.dll based on?”. Stack Overflow, 22 August 2021.
  10. [10]RPI rlt-portlib, lib/SymT/SymT_hash.c (RCS revision 1.1, 15 September 1995), as packed in the library’s shell archive Part02 (Internet Archive copy of 5 November 2004). The file’s own URL, cited in [9], was not archived.
  11. [11]V. Averkin. Funny: How the new powerful cryptography implemented in Word 2007 turns it into a perfect tool for document password removal. Aspose blogs, 20 August 2007 (Internet Archive copy of 17 July 2014).
  12. [12]StrongWind1. Office Password Toolkit: XOR obfuscation. strongwind.dev (2025–26; accessed 19 October 2026).
  13. [13]K. Hamadeh. How RC2’s π table was made. 18 October 2026. khalidhamadeh.com/writing/rc2-pi-table

Citing this note

Hamadeh, K. (2026). The Office password tables are a CRC. https://khalidhamadeh.com/writing/office-password-crc