@echo off
setlocal EnableExtensions DisableDelayedExpansion
set "SCRIPT=%~dp0SmartDecrypt.py"
if not exist "%SCRIPT%" goto script_missing
set "INPUT=%~1"
if not defined INPUT (
echo Drag a file or folder onto this BAT file.
echo Or enter the full path below.
echo.
set /p "INPUT=Input path: "
)
if not defined INPUT goto no_input
if not exist "%INPUT%" goto input_missing
where py >nul 2>&1
if errorlevel 1 goto use_python
py -3 "%SCRIPT%" "%INPUT%" --overwrite
set "CODE=%ERRORLEVEL%"
goto finished
:use_python
where python >nul 2>&1
if errorlevel 1 goto python_missing
python "%SCRIPT%" "%INPUT%" --overwrite
set "CODE=%ERRORLEVEL%"
goto finished
:script_missing
echo [ERROR] SmartDecrypt_Batch_Keep413.py was not found next to this BAT file.
set "CODE=1"
goto finished
:no_input
echo [ERROR] No input file or folder was specified.
set "CODE=1"
goto finished
:input_missing
echo [ERROR] Input path does not exist:
echo "%INPUT%"
set "CODE=1"
goto finished
:python_missing
echo [ERROR] Python was not found.
echo Install Python 3 and enable "Add Python to PATH".
set "CODE=1"
goto finished
:finished
echo.
if "%CODE%"=="0" (
echo Completed successfully.
echo The result is stored next to the source with the suffix _smart_removed.
) else (
echo Failed with exit code %CODE%.
)
echo.
pause
exit /b %CODE%
然后是PY文件
#!/usr/bin/env python3
"""Batch-remove SmartCrypt FF ED FA CE while preserving Lineage2Ver encryption.
This script replaces the original rename-to-SysString-e.dat workflow:
* recursively scans a file or directory;
* recognizes SmartCrypt containers by FF ED FA CE, regardless of filename;
* removes only the outer SmartCrypt layer;
* rebuilds the standard inner Lineage II container:
mode 1 -> Lineage2Ver111 (XOR 0xAC remains encrypted)
mode 3 -> Lineage2Ver413 (RSA + zlib remains encrypted)
* keeps original filenames and directory structure;
* copies non-SmartCrypt files unchanged by default.
The output is suitable for tools that expect the original Lineage2Ver111/413
container instead of a fully decoded raw DAT/Unreal package.
"""
from __future__ import annotations
import argparse
import csv
import os
import shutil
import struct
import sys
import tempfile
import zlib
from dataclasses import dataclass
from pathlib import Path
from typing import BinaryIO, Iterable
class SmartDecryptError(RuntimeError):
"""Expected input/format error."""
@dataclass(frozen=True)
class SmartHeader:
mode: int
protocol: int
key6: bytes
key16: bytes
@dataclass
class ReportRow:
status: str
mode: str
protocol: str
source: str
destination: str
size: int
message: str = ""
def lineage_header(protocol: int) -> bytes:
"""Build UTF-16LE Lineage2VerXXX header (28 bytes for 3-digit protocols)."""
text = f"Lineage2Ver{protocol}"
encoded = text.encode("utf-16le")
if len(encoded) != SMART_HEADER_SIZE:
raise SmartDecryptError(
f"Unexpected Lineage header size for protocol {protocol}: {len(encoded)}"
)
return encoded
def lineage_tail(crc32_value: int) -> bytes:
"""Build the standard 20-byte Lineage tail with CRC32 at offset 12."""
tail = bytearray(LINEAGE_TAIL_SIZE)
struct.pack_into("<I", tail, LINEAGE_CRC_OFFSET, crc32_value & 0xFFFFFFFF)
return bytes(tail)
def parse_smart_header(header: bytes) -> SmartHeader:
"""Decode the 28-byte FF ED FA CE header and recover its two stream keys."""
if len(header) != SMART_HEADER_SIZE:
raise SmartDecryptError(
f"Short SmartCrypt header: expected {SMART_HEADER_SIZE}, got {len(header)}"
)
if header[:4] != SMART_MAGIC:
raise SmartDecryptError("Not an FF ED FA CE SmartCrypt file")
mode = int.from_bytes(header[4:6], "little")
protocol = MODE_TO_PROTOCOL.get(mode)
if protocol is None:
raise SmartDecryptError(f"Unsupported SmartCrypt mode: {mode}")
mode_byte = mode & 0xFF
key6 = bytes(value ^ mode_byte for value in header[6:12])
key16 = bytes(header[12 + i] ^ key6[i % 6] for i in range(16))
return SmartHeader(mode=mode, protocol=protocol, key6=key6, key16=key16)
def make_stream_table(key6: bytes, key16: bytes) -> bytes:
"""Precompute one full period of the positional SmartCrypt XOR stream."""
if len(key6) != 6 or len(key16) != 16:
raise SmartDecryptError("Invalid SmartCrypt key lengths")
def remove_smartcrypt(
source: Path,
destination: Path,
*,
overwrite: bool = False,
chunk_size: int = DEFAULT_CHUNK_SIZE,
) -> SmartHeader:
"""Remove only FFEDFACE and rebuild Lineage2Ver111/413 around the inner body."""
file_size = source.stat().st_size
minimum_size = SMART_HEADER_SIZE + SMART_TAIL_SIZE
if file_size < minimum_size:
raise SmartDecryptError(
f"File is too small: {file_size} bytes, expected at least {minimum_size}"
)
if destination.exists() and not overwrite:
raise SmartDecryptError(f"Destination already exists: {destination}")
if chunk_size <= 0:
raise SmartDecryptError("Chunk size must be positive")
return smart_header
finally:
if temporary_path is not None:
try:
temporary_path.unlink(missing_ok=True)
except OSError:
pass
def verify_lineage_container(path: Path, expected_protocol: int | None = None) -> None:
"""Verify rebuilt header, total size and CRC32 without removing inner encryption."""
size = path.stat().st_size
if size < SMART_HEADER_SIZE + LINEAGE_TAIL_SIZE:
raise SmartDecryptError(f"Output is too small: {path}")
with path.open("rb") as stream:
header = stream.read(SMART_HEADER_SIZE)
try:
header_text = header.decode("utf-16le")
except UnicodeDecodeError as exc:
raise SmartDecryptError(f"Invalid Lineage header in {path}") from exc
if not header_text.startswith("Lineage2Ver"):
raise SmartDecryptError(f"Missing Lineage2Ver header in {path}")
try:
protocol = int(header_text[len("Lineage2Ver") :])
except ValueError as exc:
raise SmartDecryptError(f"Invalid protocol in {path}: {header_text!r}") from exc
if expected_protocol is not None and protocol != expected_protocol:
raise SmartDecryptError(
f"Protocol mismatch in {path}: expected {expected_protocol}, got {protocol}"
)
remaining_without_tail = size - SMART_HEADER_SIZE - LINEAGE_TAIL_SIZE
crc = zlib.crc32(header)
while remaining_without_tail:
chunk = stream.read(min(DEFAULT_CHUNK_SIZE, remaining_without_tail))
if not chunk:
raise SmartDecryptError(f"Unexpected end of rebuilt file: {path}")
crc = zlib.crc32(chunk, crc)
remaining_without_tail -= len(chunk)
input_resolved = input_path.resolve()
output_resolved = output_path.resolve()
if input_resolved == output_resolved:
raise SmartDecryptError("Input and output paths must be different")
is_single_file = input_path.is_file()
if not is_single_file:
output_path.mkdir(parents=True, exist_ok=True)
for source in iter_files(input_path, recursive):
# Protect against an output folder located inside the scanned input tree.
try:
source.resolve().relative_to(output_resolved)
continue
except ValueError:
pass
relative = Path(source.name) if is_single_file else source.relative_to(input_path)
destination = output_path if is_single_file else output_path / relative
try:
info = smart_file_info(source)
if info is not None:
result = remove_smartcrypt(
source, destination, overwrite=overwrite
)
if verify:
verify_lineage_container(destination, result.protocol)
print(
f"[SMART] mode={result.mode} -> Lineage2Ver{result.protocol}: "
f"{relative}"
)
rows.append(
ReportRow(
status="decrypted",
mode=str(result.mode),
protocol=str(result.protocol),
source=str(source),
destination=str(destination),
size=destination.stat().st_size,
)
)
elif copy_plain:
destination.parent.mkdir(parents=True, exist_ok=True)
if destination.exists() and not overwrite:
raise SmartDecryptError(f"Destination already exists: {destination}")
shutil.copy2(source, destination)
print(f"[COPY] {relative}")
rows.append(
ReportRow(
status="copied",
mode="",
protocol="",
source=str(source),
destination=str(destination),
size=destination.stat().st_size,
)
)
else:
print(f"[SKIP] {relative}")
rows.append(
ReportRow(
status="skipped",
mode="",
protocol="",
source=str(source),
destination="",
size=source.stat().st_size,
message="Not an FF ED FA CE SmartCrypt file",
)
)
except (OSError, SmartDecryptError) as exc:
errors += 1
print(f"[ERROR] {relative}: {exc}", file=sys.stderr)
rows.append(
ReportRow(
status="error",
mode="",
protocol="",
source=str(source),
destination=str(destination),
size=source.stat().st_size if source.exists() else 0,
message=str(exc),
)
)
return rows, errors
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description=(
"Recursively remove FFEDFACE SmartCrypt while preserving inner "
"Lineage2Ver111/413 encryption and original filenames."
)
)
parser.add_argument("input", type=Path, help="Input file or directory")
parser.add_argument(
"output",
type=Path,
nargs="?",
help="Output file/directory; default: <input>_smart_removed",
)
parser.add_argument(
"--no-recursive",
action="store_true",
help="Process only files directly inside the input directory",
)
parser.add_argument(
"--smart-only",
action="store_true",
help="Do not copy files that are not FFEDFACE SmartCrypt",
)
parser.add_argument(
"--overwrite",
action="store_true",
help="Overwrite existing output files",
)
parser.add_argument(
"--no-verify",
action="store_true",
help="Skip output Lineage header/CRC verification",
)
return parser