A command-line tool for decrypting Logan mobile log files.
Logan is Meituan's open-source mobile logging framework, used by Android
and iOS apps for compact encrypted log collection. logandump reads a
Logan binary log file, decrypts each record with AES (CBC or CTR),
strips the PKCS7 padding, decompresses the gzip payload, and writes the
resulting JSON log lines to stdout or to a file.
- CBC and CTR modes — supports both the vanilla Meituan format and the OPPO CTR-mode variant observed in the wild
- Key and IV as file — read secrets from files so they don't leak
via
ps(--key-file,--iv-file) - Lenient with statistics — bad records are skipped with a per- record stderr warning and a final summary, so a partially-corrupted file still yields whatever records survive
- Pipeable output — raw JSON stream to stdout, ready for
jqorgrep - Safe defaults — write-binary output (no accidental appending),
SIGPIPE restored (no traceback when piped to
head)
- Python 3.10+ (uses built-in generic syntax
list[str],str | None) - pycryptodome — the AES
primitive. Install with
python3 -m pip install pycryptodome.
pycryptodome IS published to PyPI (unlike some other crypto libraries
of a similar name, and unlike Meituan's MMKV which is source-build only).
A single pip install should suffice.
# For your default python3
python3 -m pip install pycryptodome
# ...or for a specific interpreter if you have multiple
python3.11 -m pip install pycryptodomeVerify under the interpreter you plan to use:
python3.11 -c "from Crypto.Cipher import AES; print(AES)"Pick the pattern that matches your Python setup.
If python3 -c "from Crypto.Cipher import AES" works, the script's
shebang (#!/usr/bin/env python3) will find the right interpreter. Just
symlink the script onto your $PATH:
ln -s "$(pwd)/logandump.py" /usr/local/bin/logandumpIf your default python3 doesn't have pycryptodome, create a small
bash wrapper that pins the correct interpreter. Save the following as
~/bin/logandump (or anywhere on your $PATH):
#!/bin/bash
exec python3.11 /path/to/logan-dump/logandump.py "$@"Then make it executable:
chmod +x ~/bin/logandumpTune to taste:
- Replace
python3.11with the interpreter wherepycryptodomeactually lives (~/.venvs/logan/bin/python,/opt/homebrew/bin/python3.11, …) - Replace
/path/to/logan-dump/logandump.pywith the absolute path to the script in this repository
Two details in the wrapper matter:
execreplaces the bash process with Python, avoiding a dangling shell and making signal handling (Ctrl-C, SIGPIPE) behave correctly"$@"— quote it! Without the quotes, arguments containing spaces get re-split by the shell, so a path like-f "~/Desktop/some dir/logan.log"silently breaks
logandump --version
logandump --helplogandump -f <logan-file> (--key KEY | --key-file PATH) (--iv IV | --iv-file PATH) [options]
| Flag | Description |
|---|---|
-f <path> |
Required. Path to the Logan binary log file |
-o <path> |
Write decoded JSON to a file (default: stdout) |
-m {CBC,CTR} |
AES mode (default: CBC; use CTR for OPPO-produced files) |
-k, --key <str> |
AES key as a literal string — visible via ps |
--key-file <path> |
Read the AES key from a file (preferred) |
-i, --iv <str> |
AES IV as a literal string — visible via ps |
--iv-file <path> |
Read the AES IV from a file (preferred) |
--version |
Print version and exit |
Exactly one of --key/--key-file and one of --iv/--iv-file must
be supplied.
Both key and IV must encode to exactly 16 bytes of UTF-8.
In the snippets below, KEY16 / IV16 stand for your own 16-byte
AES key and IV — logandump does not ship with any credentials.
# Decrypt an OPPO Logan file (CTR mode) to stdout
logandump -m CTR --key KEY16 --iv IV16 -f test.log
# Pipe through jq to pretty-print each record
logandump -m CTR --key KEY16 --iv IV16 -f test.log | jq -c '.'
# Write to a file instead of stdout
logandump -m CTR --key KEY16 --iv IV16 -f test.log -o out.json
# Read key and IV from files (avoids leaking via `ps`)
printf '%s' 'YOUR_16_BYTE_KEY' > ~/.logan.key
printf '%s' 'YOUR_16_BYTE_IV' > ~/.logan.iv
chmod 600 ~/.logan.key ~/.logan.iv # don't leave secrets world-readable
logandump -m CTR --key-file ~/.logan.key --iv-file ~/.logan.iv -f test.log
# Decrypt a vanilla Meituan file (CBC mode is the default)
logandump --key KEY16 --iv IV16 -f logan.log
# Filter for a specific thread
logandump -m CTR --key KEY16 --iv IV16 -f test.log | jq -c 'select(.n == "JsThread")'| Code | Meaning |
|---|---|
0 |
All records decrypted successfully, OR some failed but some succeeded |
1 |
Fatal error: file not found, wrong key length, or ZERO records decrypted (usually wrong key/IV/mode) |
2 |
argparse rejected the command line (missing required option, etc.) |
Logan's binary format is not documented in the upstream README or wiki.
The format understanding for this tool was derived from the C reference
implementation in Meituan's repository at
Logan/Clogan/ — specifically the restore_last_position_clogan
function in clogan_core.c and clogan_zlib_end_compress in
zlib_util.c.
The per-record layout on disk is:
[1 byte ] 0x01 LOGAN_WRITE_PROTOCOL_HEADER
[4 bytes] content_len (big-endian u32)
[N bytes] encrypted content AES(gzip(JSON lines) + PKCS7)
[1 byte ] 0x00 LOGAN_WRITE_PROTOCOL_TAIL
The decryption pipeline per record:
- Read marker byte and verify it is
0x01. - Read the 4-byte big-endian length.
- Read
lengthbytes of encrypted content. - Read and discard the trailing
0x00tail byte (it is always present and is NOT counted inlength). - AES-decrypt the encrypted bytes using a fresh cipher (Meituan's writer resets the IV to the master value at each record start).
- Strip PKCS7 padding using the last byte as the length (always 1-16 by the writer's invariant, never 0).
gzip.decompress()the remaining bytes to get the raw JSON log stream, each record terminated by\n.- Write the decompressed bytes to the output stream as-is.
Each gzip chunk decodes to one or more newline-terminated JSON objects
with the Logan field schema: c (content), f (flag), l (local
time in ms epoch), n (thread name), i (thread id), and optionally
m (is-main-thread).
See CLAUDE.md in this repository for the full design-decision
rationale, including why PKCS7 length is guaranteed to be in [1, 16],
why CTR mode shares CBC's framing in the OPPO variant, and how this
tool was verified against a real OPPO test file.
MIT — see LICENSE.