Skip to content

PST Body Cipher (NDB Permute / Cyclic)

⚪ Keyless Obfuscation — non-password data-block encoding in Outlook Personal Folders (.pst) files

Overview

  • Per-file content obfuscation applied to end-user data blocks inside .pst files; no password, no key material stored in the file
  • A single HEADER byte bCryptMethod selects one of four modes: no encoding (0x00), fixed monoalphabetic substitution (0x01, Permute), per-block keyed substitution stream (0x02, Cyclic), or WIP/EDP (0x10)
  • Only external data blocks are encoded; the HEADER, allocation-metadata pages, and all BTree pages are stored without obfuscation
  • Applies to both PST format variants: ANSI (Outlook 97-2002, wVer 14-15) and Unicode (Outlook 2003+, wVer >= 23)
  • Used by: Outlook
  • Spec: [MS-PST] SS2.2.2.6 (HEADER), SS5.1 (CryptPermute), SS5.2 (CryptCyclic)

Decode Flow

flowchart LR
  A["📄 .pst File\nRead HEADER"] --> B{"bCryptMethod?"}
  B -->|"0x00 NONE"| C["No decoding needed\nBlocks are plaintext"]
  B -->|"0x01 PERMUTE"| D["Walk BBT\nFor each external block"]
  B -->|"0x02 CYCLIC"| E["Walk BBT\nFor each external block"]
  B -->|"0x10 WIP"| F["OS-held key\n(outside spec)"]
  D --> G["CryptPermute\npb[i] = mpbbI[pb[i]]\n(fixed table lookup)"]
  E --> H["dwKey = low32(BID)\nw = dwKey XOR (dwKey>>16)"]
  H --> I["CryptCyclic\nmpbbR → mpbbS → mpbbI\nw increments per byte"]
  G --> J["Decoded data block"]
  I --> J

On-Disk Structure

flowchart LR
  subgraph HDR["HEADER (offset 0)"]
    direction TB
    MAG["dwMagic = !BDN\nwVer, wMagicClient"]
    SENT["bSentinel = 0x80\nbCryptMethod (1 byte)\n0x00 / 0x01 / 0x02 / 0x10"]
    ROOT["ROOT structure\nBREFNBT, BREFBBT"]
    MAG --> SENT --> ROOT
  end
  subgraph BBT["Block BTree (unencrypted)"]
    direction TB
    BBTE1["BBTENTRY\nbid, ib (file offset), cb"]
    BBTE2["BBTENTRY\nbid, ib, cb"]
    BBTE3["..."]
    BBTE1 --- BBTE2 --- BBTE3
  end
  subgraph BLK["Data Blocks"]
    direction TB
    DATA["data (cb bytes)\nENCODED region"]
    PAD["padding\n(64-byte alignment)\nNOT encoded"]
    TRAIL["BLOCKTRAILER\ncb, wSig, dwCRC, bid\nNOT encoded"]
    DATA --> PAD --> TRAIL
  end
  ROOT --> BBTE1
  BBTE1 -->|"ib offset"| DATA

Quick Reference

Parameter Value Notes
bCryptMethod 1 byte at offset 0x0201 (Unicode) / 0x01CD (ANSI) Immediately after bSentinel (0x80)
Permute key None (fixed table) mpbbR encodes, mpbbI decodes
Cyclic key low32(BID) per block Public metadata from unencrypted BBT
Encoded region data field only (up to 8176 B) Padding and BLOCKTRAILER are never encoded
Internal blocks Skipped (BID bit 1 = 1) Only external data blocks are encoded
Password N/A Body cipher is independent of PST UI password
Strength None ([MS-PST] SS4.1) "only provide data obfuscation"

Extraction Target

What your code needs from the .pst file to decode all data blocks:

Field Source Location Type Example Value
bCryptMethod HEADER offset 0x0201 (Unicode) / 0x01CD (ANSI) uint8 0x02 (Cyclic)
bSentinel HEADER offset 0x0200 (Unicode) / 0x01CC (ANSI) uint8 0x80 (anchor)
wVer HEADER offset 0x000A uint16 LE 23 (Unicode)
BREFBBT HEADER ROOT structure BID + IB (locates BBT root page)
Per-block bid BBTENTRY.BREF.bid in BBT uint64 (Unicode) / uint32 (ANSI) 0x0000_0084
Per-block ib BBTENTRY.BREF.ib in BBT uint64 / uint32 (absolute file offset)
Per-block cb BBTENTRY.cb uint16 4096
BID i bit bid & 0x02 flag 0 = external (encode), 1 = internal (skip)

Python function signature this maps to:

def decode_pst_block(
    data: bytearray,  # raw data field bytes (cb bytes)
    crypt_method: int,  # from HEADER.bCryptMethod (0x00/0x01/0x02)
    bid: int,  # from BBTENTRY.BREF.bid (used as dwKey for cyclic)
    mpbb_crypt: bytes,  # 768-byte substitution table from [MS-PST] SS5.1
) -> bytearray: ...  # returns decoded data in-place

1. What It Is and Where It Is Used

  • The PST body cipher is a per-file content obfuscation applied to end-user data blocks inside a Microsoft Outlook Personal Folders file (.pst).
  • It is not password-derived and carries no cryptographic key material in the file.
  • A single header byte, bCryptMethod ([MS-PST] section 2.2.2.6, HEADER structure), selects one of four modes: no encoding, a fixed monoalphabetic byte substitution (Permutative Encoding), a per-block keyed substitution stream (Cyclic Encoding), or Windows Information Protection (WIP/EDP) encryption.
  • The first three modes are fully documented in [MS-PST]; the fourth delegates to the operating system.

The Permutative and Cyclic ciphers apply to both PST format variants: ANSI (Outlook 97-2002, wVer 14-15) and Unicode (Outlook 2003+, wVer >= 23) ([MS-PST] section 2.2.2.6). Only non-internal (external) data blocks are encoded; the HEADER, allocation-metadata pages (AMap, PMap, FMap, FPMap, DList), and BTree pages (NBT, BBT) are stored without obfuscation ([MS-PST] section 4.1, section 2.2.2.8.3.1.1).

[MS-PST] section 4.1 states: "This file format specification uses two keyless cipher algorithms to encode the data blocks in the PST. These algorithms only provide data obfuscation and can be conveniently decoded once the exact encoding algorithm is understood."

The PST body cipher is entirely distinct from the PST UI password (a CRC-32 hash stored as PidTagPstPassword, property tag 0x67FF), which is documented in [MS-PST] section 2.4.3.3. The UI password does not key or gate the body cipher in any way ([MS-PST] section 4.2).

Related documents:

Governing spec sections:

Section Content
[MS-PST] section 2.2.2.6 HEADER structure: bSentinel, bCryptMethod field and selector table
[MS-PST] section 2.2.2.2 BID (Block ID) structure
[MS-PST] section 2.2.2.8 Blocks: data blocks, BLOCKTRAILER
[MS-PST] section 2.2.2.8.3.1 Data Blocks
[MS-PST] section 2.2.2.8.3.1.1 Data Block Encoding/Obfuscation
[MS-PST] section 2.2.2.7.7 BTrees (NBT, BBT)
[MS-PST] section 2.2.2.7.7.3 BBTENTRY (Leaf BBT Entry)
[MS-PST] section 4.1 Strength of Encoded PST Data Blocks (security)
[MS-PST] section 4.2 Strength of PST Password (security, UI password)
[MS-PST] section 5.1 Permutative Encoding: CryptPermute, mpbbCrypt[768]
[MS-PST] section 5.2 Cyclic Encoding: CryptCyclic

2. Profiles / Parameters Table

The profile is selected by the single bCryptMethod byte in the HEADER. There is no hash, no salt, no iteration count, and no password-derived key in any profile ([MS-PST] section 4.1).

bCryptMethod Friendly name Cipher type Key input Self-inverse? Spec section
0x00 NDB_CRYPT_NONE None None N/A section 2.2.2.6
0x01 NDB_CRYPT_PERMUTE Fixed 256-byte monoalphabetic substitution Fixed table mpbbR (encode) / mpbbI (decode) No (mpbbI = mpbbR^-1; encode and decode use different tables) section 5.1
0x02 NDB_CRYPT_CYCLIC Per-byte keyed substitution stream Low 32 bits of the data block's BID (public BBT metadata, not a secret) Yes (same function encodes and decodes) section 5.2
0x10 NDB_CRYPT_EDPCRYPTED Windows Information Protection (WIP/EDP) OS-held enterprise/user key, not in the file N/A section 2.2.2.6

Only non-internal (external) data blocks are encoded. Internal blocks (those with the i bit set in their BID, [MS-PST] section 2.2.2.2), the HEADER, allocation-metadata pages, and all BTree pages are stored without obfuscation ([MS-PST] section 4.1, section 2.2.2.8.3.1.1).

3. Byte Layout / On-Disk Structures

3.1 HEADER — bCryptMethod Location ([MS-PST] section 2.2.2.6)

The bCryptMethod field is located in the HEADER structure at file offset 0. The field immediately follows bSentinel (which MUST be 0x80) and is preceded by the FMap (rgbFM, 128 bytes) and FPMap (rgbFP, 128 bytes) regions. The absolute offset is derivable from the spec-defined field sizes and ordering.

Unicode HEADER (partial, showing the relevant tail):

Field Offset Size Type Description
dwMagic 0x0000 4 DWORD MUST be { 0x21, 0x42, 0x44, 0x4E } ("!BDN")
dwCRCPartial 0x0004 4 DWORD CRC-32 of 471 bytes from wMagicClient
wMagicClient 0x0008 2 WORD MUST be { 0x53, 0x4D }
wVer 0x000A 2 WORD >= 23 for Unicode; 37 indicates WIP-capable
wVerClient 0x000C 2 WORD Client format version (19 for current spec)
bPlatformCreate 0x000E 1 BYTE MUST be 0x01
bPlatformAccess 0x000F 1 BYTE MUST be 0x01
dwReserved1 0x0010 4 DWORD Reserved (zero)
dwReserved2 0x0014 4 DWORD Reserved (zero)
bidUnused 0x0018 8 ULONGLONG Unused padding (Unicode only)
bidNextP 0x0020 8 ULONGLONG Next page BID
dwUnique 0x0028 4 DWORD Monotonic modification counter
rgnid[] 0x002C 128 NID[32] NID counters per NID_TYPE
qwUnused 0x00AC 8 ULONGLONG MUST be zero (Unicode only)
root 0x00B4 72 ROOT ROOT structure (section 2.2.2.5)
dwAlign 0x00FC 4 DWORD Alignment padding (Unicode only)
rgbFM 0x0100 128 BYTE[128] Deprecated FMap (filled with 0xFF)
rgbFP 0x0180 128 BYTE[128] Deprecated FPMap (filled with 0xFF)
bSentinel 0x0200 1 BYTE MUST be 0x80
bCryptMethod 0x0201 1 BYTE Encoding selector (0x00/0x01/0x02/0x10)
rgbReserved 0x0202 2 WORD Reserved (zero)
bidNextB 0x0204 8 ULONGLONG Next block BID (increments by 4)
dwCRCFull 0x020C 4 DWORD CRC-32 of 516 bytes from wMagicClient to bidNextB
rgbReserved2 0x0210 3 BYTE[3] Reserved
bReserved 0x0213 1 BYTE Reserved
rgbReserved3 0x0214 32 BYTE[32] Reserved

ANSI HEADER (partial, showing the relevant tail):

Field Offset Size Type Description
dwMagic 0x0000 4 DWORD MUST be { 0x21, 0x42, 0x44, 0x4E } ("!BDN")
dwCRCPartial 0x0004 4 DWORD CRC-32 of 471 bytes from wMagicClient
wMagicClient 0x0008 2 WORD MUST be { 0x53, 0x4D }
wVer 0x000A 2 WORD 14 or 15 for ANSI
wVerClient 0x000C 2 WORD Client format version
bPlatformCreate 0x000E 1 BYTE MUST be 0x01
bPlatformAccess 0x000F 1 BYTE MUST be 0x01
dwReserved1 0x0010 4 DWORD Reserved (zero)
dwReserved2 0x0014 4 DWORD Reserved (zero)
bidNextB 0x0018 4 DWORD Next block BID (ANSI position)
bidNextP 0x001C 4 DWORD Next page BID
dwUnique 0x0020 4 DWORD Monotonic modification counter
rgnid[] 0x0024 128 NID[32] NID counters per NID_TYPE
root 0x00A4 40 ROOT ROOT structure (ANSI, section 2.2.2.5)
rgbFM 0x00CC 128 BYTE[128] Deprecated FMap (filled with 0xFF)
rgbFP 0x014C 128 BYTE[128] Deprecated FPMap (filled with 0xFF)
bSentinel 0x01CC 1 BYTE MUST be 0x80
bCryptMethod 0x01CD 1 BYTE Encoding selector (0x00/0x01/0x02/0x10)
rgbReserved 0x01CE 2 WORD Reserved (zero)
ullReserved 0x01D0 8 ULONGLONG Reserved (ANSI only)
dwReserved 0x01D8 4 DWORD Reserved (ANSI only)
rgbReserved2 0x01DC 3 BYTE[3] Reserved
bReserved 0x01DF 1 BYTE Reserved
rgbReserved3 0x01E0 32 BYTE[32] Reserved

The absolute offsets are computed from the spec-defined field ordering and sizes in [MS-PST] section 2.2.2.6: 0x0201 (Unicode) and 0x01CD (ANSI). The bSentinel field at 0x80 immediately preceding bCryptMethod serves as a reliable anchor byte.

3.2 BID (Block ID) Structure ([MS-PST] section 2.2.2.2)

The BID identifies every allocated block in the PST. It is the source of the Cyclic Encoding key.

Unicode BID (8 bytes):

Bit(s) Field Description
0 r Reserved. MUST be zero before BBT lookup
1 i Internal flag: 1 = internal (metadata) block, 0 = external (data) block
2-63 bidIndex Monotonically increasing unique block identifier

ANSI BID (4 bytes):

Bit(s) Field Description
0 r Reserved. MUST be zero before BBT lookup
1 i Internal flag: 1 = internal, 0 = external
2-31 bidIndex Monotonically increasing unique block identifier

Block BIDs (as used in section 2.2.2.8) reserve the two least significant bits for flags and increment by 4 each time a new one is assigned. Page BIDs (section 2.2.2.7) use all bits and increment by 1 ([MS-PST] section 2.2.2.2).

For CryptCyclic, dwKey is the low DWORD of the BID associated with the data block ([MS-PST] section 5.2). This is the full 32-bit value including the flag bits.

3.3 BBTENTRY (Leaf BBT Entry) ([MS-PST] section 2.2.2.7.7.3)

The BBT maps every allocated block to its file location. BBT entries for data blocks provide the BID (and thus the cyclic key) and the block's physical location. The BBT itself is stored unencrypted.

Unicode BBTENTRY (24 bytes):

Field Offset Size Type Description
BREF 0x00 16 BREF Contains the block's bid (8 bytes) and ib (8 bytes, absolute file offset)
cb 0x10 2 WORD Byte count of raw data in the block (excluding trailer and padding)
cRef 0x12 2 WORD Reference count
dwPadding 0x14 4 DWORD Padding (zero)

ANSI BBTENTRY (12 bytes):

Field Offset Size Type Description
BREF 0x00 8 BREF Contains the block's bid (4 bytes) and ib (4 bytes, absolute file offset)
cb 0x08 2 WORD Byte count of raw data in the block
cRef 0x0A 2 WORD Reference count

3.4 Data Block Layout ([MS-PST] section 2.2.2.8.3.1)

A data block is an external (non-internal) block containing higher-layer data. Its maximum size is 8192 bytes. The encoded region is only the data field; the padding and blockTrailer are never encoded ([MS-PST] section 2.2.2.8.3.1.1).

Field Size Description
data cb bytes (variable, up to 8176 bytes Unicode / 8180 bytes ANSI) Payload data. This is the encoded/decoded region.
padding Variable (0-63 bytes) Alignment padding to make total size a multiple of 64. Not encoded.
blockTrailer 16 bytes (Unicode) / 12 bytes (ANSI) BLOCKTRAILER structure. Not encoded.

BLOCKTRAILER ([MS-PST] section 2.2.2.8.1):

Field Size (both formats) Description
cb 2 bytes Byte count of data (excluding trailer and padding)
wSig 2 bytes Block signature (section 5.5)
dwCRC 4 bytes CRC-32 of the cb bytes of raw data
bid 8 bytes (Unicode) / 4 bytes (ANSI) BID of the data block

Note: in the Unicode BLOCKTRAILER, the field order is cb, wSig, dwCRC, bid. In ANSI, the order is cb, wSig, bid, dwCRC ([MS-PST] section 2.2.2.8.1).

The dwCRC in the BLOCKTRAILER is computed over the raw (encoded) data bytes, not the decoded plaintext. Encoding and decoding are performed in-place on the data field only ([MS-PST] section 2.2.2.8.3.1.1).

3.5 Which Blocks Are Encoded

Per [MS-PST] section 2.2.2.8.3.1.1: "A special case exists when a PST file is configured to encode its contents. In that case, the NDB Layer encodes the data field of data blocks to obfuscate the data using one of two keyless ciphers." And per [MS-PST] section 4.1: "only end-user data blocks are encoded in the PST. All the other infrastructure information, including the header, allocation metadata pages and BTree pages are stored without obfuscation."

The i bit (bit 1) of the BID distinguishes internal blocks from external data blocks ([MS-PST] section 2.2.2.2). Only blocks with i = 0 (external / data blocks) are subject to encoding. Internal blocks (XBLOCKs, XXBLOCKs, SLBLOCKs, SIBLOCKs) have i = 1 and are never encoded.

4. Key Derivation

There is no password, no salt, and no iterated KDF. The "key" for each mode is either fixed (Permute) or derived from public file metadata (Cyclic).

4.1 Mode Selection ([MS-PST] section 2.2.2.6)

FUNCTION select_cipher(header):
    m = header.bCryptMethod

    IF m == 0x00 (NDB_CRYPT_NONE):
        // Data blocks stored in the clear. No encoding.
        RETURN plaintext

    IF m == 0x01 (NDB_CRYPT_PERMUTE):
        // Key is the fixed mpbbCrypt table.
        // Encode with mpbbR (bytes 0-255), decode with mpbbI (bytes 512-767).
        RETURN permute

    IF m == 0x02 (NDB_CRYPT_CYCLIC):
        // Key is the low 32 bits of each block's BID, read from the BBT.
        RETURN cyclic

    IF m == 0x10 (NDB_CRYPT_EDPCRYPTED):
        // Key is held by the OS via Windows Information Protection.
        // Not derivable from the PST file.
        RETURN wip_encrypted

4.2 Permutative Key ([MS-PST] section 5.1)

No derivation. The encode table is mpbbR (bytes 0-255 of mpbbCrypt); the decode table is mpbbI (bytes 512-767 of mpbbCrypt). Both tables are fixed constants defined in the spec. mpbbI is the inverse permutation of mpbbR: for every byte value x, mpbbI[mpbbR[x]] == x.

4.3 Cyclic Key ([MS-PST] section 5.2)

For each data block:

// dwKey is the low DWORD of the block's BID.
// Per [MS-PST] section 5.2: "the value to use for dwKey is the
// lower DWORD of the BID associated with this data block."

dwKey = low32(block.BID)

// Derive the initial 16-bit mixer word:
w = (WORD)(dwKey XOR (dwKey >> 16))

// w then increments by 1 for every byte processed (see section 5).

The BID is public metadata stored in the unencrypted BBT ([MS-PST] section 2.2.2.7.7.3); it is not a secret.

4.4 Table Decomposition ([MS-PST] section 5.1)

The mpbbCrypt blob is 768 bytes consisting of three concatenated 256-byte substitution tables:

#define mpbbR   (mpbbCrypt)          // bytes   0-255:  forward substitution (encode)
#define mpbbS   (mpbbCrypt + 256)    // bytes 256-511:  involution (used in cyclic mixer)
#define mpbbI   (mpbbCrypt + 512)    // bytes 512-767:  inverse of mpbbR (decode)

Properties: - mpbbI[mpbbR[x]] == x for all x in 0..255 (mpbbI is the inverse permutation of mpbbR) - mpbbS[mpbbS[x]] == x for all x in 0..255 (mpbbS is a self-inverse involution)

These properties are what make CryptCyclic a symmetric (self-inverse) cipher: the composition mpbbI(mpbbS(mpbbR(...))) with the additive/subtractive mixer structure reverses itself when applied a second time with the same incrementing w value.

5. Verification / Decryption — Pseudocode

There is no verifier, no MAC, and no password check gating the body cipher. Decoding is unconditional: read the bCryptMethod byte, walk the BBT to locate non-internal data blocks, and apply the appropriate transform to each block's data field. The decoded result is the raw NDB/LTP layer data.

5.1 CryptPermute ([MS-PST] section 5.1)

Monoalphabetic byte substitution. fEncrypt = TRUE encodes (via mpbbR); fEncrypt = FALSE decodes (via mpbbI). The transform is applied in-place. The spec's DWORD-at-a-time loop is a speed optimization; semantically each byte is independently substituted.

FUNCTION CryptPermute(pv, cb, fEncrypt):
    // pv: pointer to data buffer
    // cb: byte count of data
    // fEncrypt: TRUE to encode, FALSE to decode

    pb = (byte *)pv
    pbTable = fEncrypt ? mpbbR : mpbbI

    // Simplified single-byte loop (equivalent to the spec's optimized version):
    FOR i = 0 TO cb - 1:
        pb[i] = pbTable[pb[i]]

The spec ([MS-PST] section 5.1) provides an optimized C++ implementation that aligns to DWORD boundaries and processes 4 bytes per iteration for performance. The algorithm is:

void CryptPermute(PVOID pv, int cb, BOOL fEncrypt)
{
    byte *      pb      = (byte *)pv;
    byte *      pbTable = fEncrypt ? mpbbR : mpbbI;
    const DWORD * pdw   = (const DWORD *) pv;
    DWORD       dwCurr;
    byte        b;

    if (cb >= sizeof(DWORD))
    {
        // Align to DWORD boundary
        while (0 != (((DWORD_PTR) pb) % sizeof(DWORD)))
        {
            *pb = pbTable[*pb];
            pb++;
            cb--;
        }
        pdw = (const DWORD *) pb;
        // Process 4 bytes at a time
        for (; cb >= 4; cb -= 4)
        {
            dwCurr = *pdw;
            b = (byte) (dwCurr & 0xFF);
            *pb = pbTable[b];
            pb++;
            dwCurr = dwCurr >> 8;
            b = (byte) (dwCurr & 0xFF);
            *pb = pbTable[b];
            pb++;
            dwCurr = dwCurr >> 8;
            b = (byte) (dwCurr & 0xFF);
            *pb = pbTable[b];
            pb++;
            dwCurr = dwCurr >> 8;
            b = (byte) (dwCurr & 0xFF);
            *pb = pbTable[b];
            pb++;
            pdw++;
        }
        pb = (byte *) pdw;
    }
    // Process remaining bytes
    for (; --cb >= 0; ++pb)
        *pb = pbTable[*pb];
}

5.2 CryptCyclic ([MS-PST] section 5.2)

Per-byte keyed substitution stream. This is a symmetric cipher: the same function with the same dwKey both encodes and decodes. The dwKey is the low DWORD of the block's BID.

FUNCTION CryptCyclic(pv, cb, dwKey):
    // pv: pointer to data buffer
    // cb: byte count of data
    // dwKey: low DWORD of the block's BID

    pb = (byte *)pv
    w = (WORD)(dwKey XOR (dwKey >> 16))    // initial 16-bit mixer

    FOR i = 0 TO cb - 1:
        b = pb[i]
        b = (byte)(b + (byte)w)             // add low byte of w
        b = mpbbR[b]                         // forward substitution
        b = (byte)(b + (byte)(w >> 8))       // add high byte of w
        b = mpbbS[b]                         // involution substitution
        b = (byte)(b - (byte)(w >> 8))       // subtract high byte of w
        b = mpbbI[b]                         // inverse substitution
        b = (byte)(b - (byte)w)             // subtract low byte of w
        pb[i] = b
        w = (WORD)(w + 1)                    // advance mixer every byte

The spec's C++ implementation ([MS-PST] section 5.2):

void CryptCyclic(PVOID pv, int cb, DWORD dwKey)
{
    byte * pb = (byte *)pv;
    byte b;
    WORD w;

    w = (WORD)(dwKey ^ (dwKey >> 16));

    while (--cb >= 0) {
        b = *pb;
        b = (byte)(b + (byte)w);
        b = mpbbR[b];
        b = (byte)(b + (byte)(w >> 8));
        b = mpbbS[b];
        b = (byte)(b - (byte)(w >> 8));
        b = mpbbI[b];
        b = (byte)(b - (byte)w);
        *pb++ = b;
        w = (WORD)(w + 1);
    }
}

Self-inverse property: CryptCyclic is its own inverse because: 1. The additive mixer +w_lo / +w_hi is reversed by the subtractive mixer -w_hi / -w_lo in the same pass. 2. mpbbS is an involution (mpbbS[mpbbS[x]] == x), so it reverses itself. 3. mpbbI is the inverse of mpbbR, so the forward-then-inverse substitution pair cancels. 4. The w increment sequence is deterministic from dwKey, producing the same sequence on both encode and decode.

Running CryptCyclic twice with the same dwKey on the same data returns the original bytes.

5.3 Complete Decode Procedure

FUNCTION decode_pst_blocks(pst_file):
    // 1. Read bCryptMethod from HEADER
    header = read_header(pst_file)
    method = header.bCryptMethod

    IF method == 0x00:  // NDB_CRYPT_NONE
        RETURN  // blocks are already plaintext

    IF method == 0x10:  // NDB_CRYPT_EDPCRYPTED
        ERROR "WIP-encrypted; requires OS-held key"

    // 2. Walk the BBT to find all non-internal data blocks
    bbt_root = header.root.BREFBBT
    FOR EACH bbtentry IN walk_bbt(pst_file, bbt_root):
        bid = bbtentry.BREF.bid
        ib  = bbtentry.BREF.ib    // absolute file offset
        cb  = bbtentry.cb          // data byte count

        // Skip internal blocks (i bit = 1)
        IF (bid & 0x02) != 0:
            CONTINUE

        // 3. Read the data portion of the block
        data = read_bytes(pst_file, offset=ib, length=cb)

        // 4. Decode in place
        IF method == 0x01:  // NDB_CRYPT_PERMUTE
            CryptPermute(data, cb, fEncrypt=FALSE)
        ELSE IF method == 0x02:  // NDB_CRYPT_CYCLIC
            dwKey = low32(bid)
            CryptCyclic(data, cb, dwKey)

        // 5. data now contains decoded NDB/LTP layer content

6. Reference Constants

6.1 bCryptMethod Selector Values ([MS-PST] section 2.2.2.6)

Value   Friendly name              Meaning
-----   -------------------------  --------------------------------------------------
0x00    NDB_CRYPT_NONE             Data blocks are not encoded.
0x01    NDB_CRYPT_PERMUTE          Encoded with the Permutation algorithm (section 5.1).
0x02    NDB_CRYPT_CYCLIC           Encoded with the Cyclic algorithm (section 5.2).
0x10    NDB_CRYPT_EDPCRYPTED       Encrypted with Windows Information Protection.

6.2 HEADER Anchors

bSentinel MUST = 0x80    (immediately precedes bCryptMethod)
bCryptMethod offset:     0x0201 (Unicode PST) / 0x01CD (ANSI PST)
dwMagic MUST = { 0x21, 0x42, 0x44, 0x4E }   ("!BDN")
wMagicClient MUST = { 0x53, 0x4D }

6.3 mpbbCrypt[768] — Full Substitution Table ([MS-PST] section 5.1)

Bytes 0-255 = mpbbR (forward/encode), bytes 256-511 = mpbbS (involution), bytes 512-767 = mpbbI (inverse/decode). Reproduced verbatim from the spec.

byte mpbbCrypt[] =
{
    // mpbbR: bytes 0-255 (forward substitution / encode table)
     65,  54,  19,  98, 168,  33, 110, 187,
    244,  22, 204,   4, 127, 100, 232,  93,
     30, 242, 203,  42, 116, 197,  94,  53,
    210, 149,  71, 158, 150,  45, 154, 136,
     76, 125, 132,  63, 219, 172,  49, 182,
     72,  95, 246, 196, 216,  57, 139, 231,
     35,  59,  56, 142, 200, 193, 223,  37,
    177,  32, 165,  70,  96,  78, 156, 251,
    170, 211,  86,  81,  69, 124,  85,   0,
      7, 201,  43, 157, 133, 155,   9, 160,
    143, 173, 179,  15,  99, 171, 137,  75,
    215, 167,  21,  90, 113, 102,  66, 191,
     38,  74, 107, 152, 250, 234, 119,  83,
    178, 112,   5,  44, 253,  89,  58, 134,
    126, 206,   6, 235, 130, 120,  87, 199,
    141,  67, 175, 180,  28, 212,  91, 205,
    226, 233,  39,  79, 195,   8, 114, 128,
    207, 176, 239, 245,  40, 109, 190,  48,
     77,  52, 146, 213,  14,  60,  34,  50,
    229, 228, 249, 159, 194, 209,  10, 129,
     18, 225, 238, 145, 131, 118, 227, 151,
    230,  97, 138,  23, 121, 164, 183, 220,
    144, 122,  92, 140,   2, 166, 202, 105,
    222,  80,  26,  17, 147, 185,  82, 135,
     88, 252, 237,  29,  55,  73,  27, 106,
    224,  41,  51, 153, 189, 108, 217, 148,
    243,  64,  84, 111, 240, 198, 115, 184,
    214,  62, 101,  24,  68,  31, 221, 103,
     16, 241,  12,  25, 236, 174,   3, 161,
     20, 123, 169,  11, 255, 248, 163, 192,
    162,   1, 247,  46, 188,  36, 104, 117,
     13, 254, 186,  47, 181, 208, 218,  61,

    // mpbbS: bytes 256-511 (involution substitution)
     20,  83,  15,  86, 179, 200, 122, 156,
    235, 101,  72,  23,  22,  21, 159,   2,
    204,  84, 124, 131,   0,  13,  12,  11,
    162,  98, 168, 118, 219, 217, 237, 199,
    197, 164, 220, 172, 133, 116, 214, 208,
    167, 155, 174, 154, 150, 113, 102, 195,
     99, 153, 184, 221, 115, 146, 142, 132,
    125, 165,  94, 209,  93, 147, 177,  87,
     81,  80, 128, 137,  82, 148,  79,  78,
     10, 107, 188, 141, 127, 110,  71,  70,
     65,  64,  68,   1,  17, 203,   3,  63,
    247, 244, 225, 169, 143,  60,  58, 249,
    251, 240,  25,  48, 130,   9,  46, 201,
    157, 160, 134,  73, 238, 111,  77, 109,
    196,  45, 129,  52,  37, 135,  27, 136,
    170, 252,   6, 161,  18,  56, 253,  76,
     66, 114, 100,  19,  55,  36, 106, 117,
    119,  67, 255, 230, 180,  75,  54,  92,
    228, 216,  53,  61,  69, 185,  44, 236,
    183,  49,  43,  41,   7, 104, 163,  14,
    105, 123,  24, 158,  33,  57, 190,  40,
     26,  91, 120, 245,  35, 202,  42, 176,
    175,  62, 254,   4, 140, 231, 229, 152,
     50, 149, 211, 246,  74, 232, 166, 234,
    233, 243, 213,  47, 112,  32, 242,  31,
      5, 103, 173,  85,  16, 206, 205, 227,
     39,  59, 218, 186, 215, 194,  38, 212,
    145,  29, 210,  28,  34,  51, 248, 250,
    241,  90, 239, 207, 144, 182, 139, 181,
    189, 192, 191,   8, 151,  30, 108, 226,
     97, 224, 198, 193,  89, 171, 187,  88,
    222,  95, 223,  96, 121, 126, 178, 138,

    // mpbbI: bytes 512-767 (inverse substitution / decode table)
     71, 241, 180, 230,  11, 106, 114,  72,
    133,  78, 158, 235, 226, 248, 148,  83,
    224, 187, 160,   2, 232,  90,   9, 171,
    219, 227, 186, 198, 124, 195,  16, 221,
     57,   5, 150,  48, 245,  55,  96, 130,
    140, 201,  19,  74, 107,  29, 243, 251,
    143,  38, 151, 202, 145,  23,   1, 196,
     50,  45, 110,  49, 149, 255, 217,  35,
    209,   0,  94, 121, 220,  68,  59,  26,
     40, 197,  97,  87,  32, 144,  61, 131,
    185,  67, 190, 103, 210,  70,  66, 118,
    192, 109,  91, 126, 178,  15,  22,  41,
     60, 169,   3,  84,  13, 218,  93, 223,
    246, 183, 199,  98, 205, 141,   6, 211,
    105,  92, 134, 214,  20, 247, 165, 102,
    117, 172, 177, 233,  69,  33, 112,  12,
    135, 159, 116, 164,  34,  76, 111, 191,
     31,  86, 170,  46, 179, 120,  51,  80,
    176, 163, 146, 188, 207,  25,  28, 167,
     99, 203,  30,  77,  62,  75,  27, 155,
     79, 231, 240, 238, 173,  58, 181,  89,
      4, 234,  64,  85,  37,  81, 229, 122,
    137,  56, 104,  82, 123, 252,  39, 174,
    215, 189, 250,   7, 244, 204, 142,  95,
    239,  53, 156, 132,  43,  21, 213, 119,
     52,  73, 182,  18,  10, 127, 113, 136,
    253, 157,  24,  65, 125, 147, 216,  88,
     44, 206, 254,  36, 175, 222, 184,  54,
    200, 161, 128, 166, 153, 152, 168,  47,
     14, 129, 101, 115, 228, 194, 162, 138,
    212, 225,  17, 208,   8, 139,  42, 242,
    237, 154, 100,  63, 193, 108, 249, 236
};

7. Security Considerations

The following security assessments are stated by the spec itself.

7.1 Strength of Encoded PST Data Blocks ([MS-PST] section 4.1)

"This file format specification uses two keyless cipher algorithms to encode the data blocks in the PST. These algorithms only provide data obfuscation and can be conveniently decoded once the exact encoding algorithm is understood."

"Moreover, only end-user data blocks are encoded in the PST. All the other infrastructure information, including the header, allocation metadata pages and BTree pages are stored without obfuscation."

"In summary, the strength of the encoded PST data blocks provides no additional security beyond data obfuscation."

7.2 Strength of PST Password ([MS-PST] section 4.2)

The UI password (a separate mechanism from the body cipher, stored as PidTagPstPassword) is documented for completeness:

"The PST Password, which is stored as a property value in the message store, is a superficial mechanism that requires the client implementation to enforce the stored password. Because the password itself is not used as a key to the encoding and decoding cipher algorithms, it does not provide any security benefit to preventing the PST data to be read by unauthorized parties."

"Moreover, the password is stored as a CRC-32 hash of the original password string, which is prone to collisions and is relatively weak against a brute-force approach."

See ./pst-crc32.md for the PST UI password algorithm.

7.3 NDB_CRYPT_EDPCRYPTED ([MS-PST] section 2.2.2.6)

The NDB_CRYPT_EDPCRYPTED (0x10) mode is the only profile that uses actual cryptographic protection. It delegates to Windows Information Protection (WIP/EDP), with keys held by the operating system and enterprise infrastructure. [MS-PST] section 2.2.2.6 notes that when wVer is 37, "it indicates that the file is written by an Outlook of version that supports Windows Information Protection (WIP). The data MAY have been protected by WIP." The WIP encryption mechanism itself is outside the scope of [MS-PST].

7.4 Summary of Security Properties

  • The Permutative cipher is a fixed, keyless monoalphabetic substitution. It provides no confidentiality.
  • The Cyclic cipher is keyed by the block's own BID, which is public metadata in the unencrypted BBT. It provides no confidentiality.
  • Neither cipher is password-derived. The UI password (PidTagPstPassword) does not key or gate the body cipher.
  • All file infrastructure (HEADER, AMap, PMap, FMap, FPMap, DList, NBT, BBT) is stored in the clear.

8. Spec References

Reference Content
[MS-PST] section 2.2.2.2 BID (Block ID) structure: 64-bit (Unicode) / 32-bit (ANSI); r and i flag bits; source of cyclic dwKey
[MS-PST] section 2.2.2.4 BREF structure: maps BID to absolute file offset
[MS-PST] section 2.2.2.5 ROOT structure: contains BREFNBT and BREFBBT to locate the BTree roots
[MS-PST] section 2.2.2.6 HEADER structure: dwMagic, wVer, bSentinel (0x80), bCryptMethod (selector table), field ordering
[MS-PST] section 2.2.2.7.7 BTrees: NBT and BBT structures
[MS-PST] section 2.2.2.7.7.3 BBTENTRY (Leaf BBT Entry): BREF, cb, cRef fields
[MS-PST] section 2.2.2.8 Blocks: fundamental data storage units, 64-byte alignment, 8192-byte maximum
[MS-PST] section 2.2.2.8.1 BLOCKTRAILER: cb, wSig, dwCRC, bid fields (not encoded)
[MS-PST] section 2.2.2.8.3.1 Data Blocks: external blocks containing higher-layer data
[MS-PST] section 2.2.2.8.3.1.1 Data Block Encoding/Obfuscation: only data field encoded; padding and blockTrailer are not
[MS-PST] section 2.4.3.3 PST Password Security: PidTagPstPassword (CRC-32 hash, unrelated to body cipher)
[MS-PST] section 4.1 Strength of Encoded PST Data Blocks: "two keyless cipher algorithms ... only provide data obfuscation"
[MS-PST] section 4.2 Strength of PST Password: "superficial mechanism ... does not provide any security benefit"
[MS-PST] section 5.1 Permutative Encoding: CryptPermute function, mpbbCrypt[768] table, mpbbR/mpbbS/mpbbI decomposition
[MS-PST] section 5.2 Cyclic Encoding: CryptCyclic function, symmetric, w = dwKey ^ (dwKey >> 16), per-byte w increment

Cross-references: