1use super::compress::{decompress_raw_zlib, decompress_zstd_frames, CompressError};
2use super::crypto::{tea_decrypt_in_place, CryptoError, EcdhTeaCipher};
3use super::protocol::{
4 MAGIC_ASYNC_NO_CRYPT_ZLIB_START, MAGIC_ASYNC_NO_CRYPT_ZSTD_START, MAGIC_ASYNC_ZLIB_START,
5 MAGIC_ASYNC_ZSTD_START, MAGIC_END, MAGIC_SYNC_NO_CRYPT_ZLIB_START,
6 MAGIC_SYNC_NO_CRYPT_ZSTD_START, MAGIC_SYNC_ZLIB_START, MAGIC_SYNC_ZSTD_START,
7};
8use hex::FromHexError;
9use thiserror::Error;
10
11const BASE_KEY: u8 = 0xcc;
12const LEGACY_MAGIC_CRYPT_START: u8 = 0x01;
13const LEGACY_MAGIC_COMPRESS_CRYPT_START: u8 = 0x02;
14const LEGACY_NEW_MAGIC_CRYPT_START: u8 = 0x03;
15const LEGACY_NEW_MAGIC_COMPRESS_CRYPT_START: u8 = 0x04;
16const LEGACY_NEW_MAGIC_COMPRESS_CRYPT_START1: u8 = 0x05;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct DecodeSummary {
21 pub blocks: usize,
23 pub output_bytes: usize,
25}
26
27#[derive(Debug, Clone, Default)]
29pub struct DecodeOptions {
30 private_key: Option<[u8; 32]>,
31}
32
33impl DecodeOptions {
34 pub fn new() -> Self {
36 Self::default()
37 }
38
39 pub fn private_key(mut self, private_key: [u8; 32]) -> Self {
41 self.private_key = Some(private_key);
42 self
43 }
44
45 pub fn private_key_hex(self, private_key_hex: &str) -> Result<Self, DecodeError> {
47 Ok(self.private_key(parse_private_key_hex(private_key_hex)?))
48 }
49}
50
51#[derive(Debug, Error)]
53pub enum DecodeError {
54 #[error("private key must be 64 hex chars")]
56 InvalidPrivateKeyLength,
57 #[error("invalid private key hex: {0}")]
59 InvalidPrivateKeyHex(#[from] FromHexError),
60 #[error("invalid secp256k1 key material")]
62 InvalidKeyMaterial,
63 #[error("no valid xlog block found")]
65 NoValidBlock,
66 #[error("encrypted xlog block requires --key/--private-key")]
68 MissingPrivateKey,
69 #[error("truncated xlog block at offset {offset}: payload len {len}")]
71 TruncatedBlock {
72 offset: usize,
74 len: usize,
76 },
77 #[error("invalid xlog tail marker at offset {offset}")]
79 InvalidTail {
80 offset: usize,
82 },
83 #[error(transparent)]
85 Compress(#[from] CompressError),
86 #[error(transparent)]
88 Crypto(CryptoError),
89}
90
91impl From<CryptoError> for DecodeError {
92 fn from(value: CryptoError) -> Self {
93 match value {
94 CryptoError::InvalidKeyMaterial => Self::InvalidKeyMaterial,
95 other => Self::Crypto(other),
96 }
97 }
98}
99
100#[derive(Debug, Clone)]
101struct BlockHeader {
102 magic: u8,
103 header_len: usize,
104 len: usize,
105 seq: u16,
106 client_pubkey: [u8; 64],
107}
108
109pub fn decode_xlog(
111 input: &[u8],
112 output: &mut Vec<u8>,
113 opts: &DecodeOptions,
114) -> Result<DecodeSummary, DecodeError> {
115 let Some(mut offset) = find_log_start(input, 2) else {
116 return Err(DecodeError::NoValidBlock);
117 };
118
119 let mut blocks = 0usize;
120 let mut last_seq = 0u16;
121
122 loop {
123 if offset >= input.len() {
124 break;
125 }
126
127 if !is_good_log_buffer(input, offset, 1) {
128 let Some(fix_pos) = find_log_start(&input[offset..], 1) else {
129 break;
130 };
131 output.extend_from_slice(
132 format!("[F]decode_log_file.py decode error len={fix_pos}\n").as_bytes(),
133 );
134 offset += fix_pos;
135 }
136
137 let header = parse_header(input, offset)?;
138 let payload_start = offset + header.header_len;
139 let payload_end = payload_start + header.len;
140 if payload_end + 1 > input.len() {
141 return Err(DecodeError::TruncatedBlock {
142 offset,
143 len: header.len,
144 });
145 }
146 if input[payload_end] != MAGIC_END {
147 return Err(DecodeError::InvalidTail {
148 offset: payload_end,
149 });
150 }
151
152 if header.seq != 0
153 && header.seq != 1
154 && last_seq != 0
155 && header.seq != last_seq.wrapping_add(1)
156 {
157 output.extend_from_slice(
158 format!(
159 "[F]decode_log_file.py log seq:{}-{} is missing\n",
160 last_seq.wrapping_add(1),
161 header.seq.wrapping_sub(1)
162 )
163 .as_bytes(),
164 );
165 }
166 if header.seq != 0 {
167 last_seq = header.seq;
168 }
169
170 let payload = &input[payload_start..payload_end];
171 let decoded = decode_payload(&header, payload, opts)?;
172 output.extend_from_slice(&decoded);
173 blocks += 1;
174 offset = payload_end + 1;
175 }
176
177 if blocks == 0 {
178 return Err(DecodeError::NoValidBlock);
179 }
180
181 Ok(DecodeSummary {
182 blocks,
183 output_bytes: output.len(),
184 })
185}
186
187pub fn parse_private_key_hex(private_key_hex: &str) -> Result<[u8; 32], DecodeError> {
189 let trimmed = private_key_hex.trim();
190 let trimmed = trimmed.strip_prefix("0x").unwrap_or(trimmed);
191 if trimmed.len() != 64 {
192 return Err(DecodeError::InvalidPrivateKeyLength);
193 }
194
195 let mut out = [0u8; 32];
196 hex::decode_to_slice(trimmed, &mut out)?;
197 Ok(out)
198}
199
200fn decode_payload(
201 header: &BlockHeader,
202 payload: &[u8],
203 opts: &DecodeOptions,
204) -> Result<Vec<u8>, DecodeError> {
205 match header.magic {
206 LEGACY_MAGIC_CRYPT_START | LEGACY_NEW_MAGIC_CRYPT_START => {
207 Ok(xor_payload(payload, legacy_xor_key(header)))
208 }
209 LEGACY_MAGIC_COMPRESS_CRYPT_START | LEGACY_NEW_MAGIC_COMPRESS_CRYPT_START => {
210 let decoded = xor_payload(payload, legacy_xor_key(header));
211 Ok(decompress_raw_zlib(&decoded)?)
212 }
213 LEGACY_NEW_MAGIC_COMPRESS_CRYPT_START1 => {
214 let joined = join_legacy_compressed_segments(payload);
215 let decoded = xor_payload(&joined, legacy_xor_key(header));
216 Ok(decompress_raw_zlib(&decoded)?)
217 }
218 MAGIC_SYNC_ZLIB_START
219 | MAGIC_SYNC_NO_CRYPT_ZLIB_START
220 | MAGIC_SYNC_ZSTD_START
221 | MAGIC_SYNC_NO_CRYPT_ZSTD_START => Ok(payload.to_vec()),
222 MAGIC_ASYNC_ZLIB_START | MAGIC_ASYNC_ZSTD_START => {
223 let private_key = opts.private_key.ok_or(DecodeError::MissingPrivateKey)?;
224 let cipher =
225 EcdhTeaCipher::new_with_server_private_key(header.client_pubkey, private_key)?;
226 let mut decoded = payload.to_vec();
227 let block_end = decoded.len() / 8 * 8;
228 tea_decrypt_in_place(&mut decoded[..block_end], &cipher.tea_key_words());
229 match header.magic {
230 MAGIC_ASYNC_ZLIB_START => Ok(decompress_raw_zlib(&decoded)?),
231 MAGIC_ASYNC_ZSTD_START => Ok(decompress_zstd_frames(&decoded)?),
232 _ => unreachable!(),
233 }
234 }
235 MAGIC_ASYNC_NO_CRYPT_ZLIB_START => Ok(decompress_raw_zlib(payload)?),
236 MAGIC_ASYNC_NO_CRYPT_ZSTD_START => Ok(decompress_zstd_frames(payload)?),
237 _ => Err(DecodeError::NoValidBlock),
238 }
239}
240
241fn parse_header(input: &[u8], offset: usize) -> Result<BlockHeader, DecodeError> {
242 let magic = input
243 .get(offset)
244 .copied()
245 .ok_or(DecodeError::NoValidBlock)?;
246 let Some((header_len, crypt_key_len)) = header_lengths_for_magic(magic) else {
247 return Err(DecodeError::NoValidBlock);
248 };
249 if offset + header_len > input.len() {
250 return Err(DecodeError::TruncatedBlock { offset, len: 0 });
251 }
252
253 let len_offset = offset + header_len - crypt_key_len - 4;
254 let len = u32::from_le_bytes([
255 input[len_offset],
256 input[len_offset + 1],
257 input[len_offset + 2],
258 input[len_offset + 3],
259 ]) as usize;
260
261 let seq = if header_len >= 1 + 2 + 1 + 1 + 4 {
262 let seq_offset = offset + header_len - crypt_key_len - 4 - 2 - 2;
263 u16::from_le_bytes([input[seq_offset], input[seq_offset + 1]])
264 } else {
265 0
266 };
267
268 let mut client_pubkey = [0u8; 64];
269 if crypt_key_len == 64 {
270 let key_start = offset + header_len - crypt_key_len;
271 client_pubkey.copy_from_slice(&input[key_start..key_start + 64]);
272 }
273
274 Ok(BlockHeader {
275 magic,
276 header_len,
277 len,
278 seq,
279 client_pubkey,
280 })
281}
282
283fn header_lengths_for_magic(magic: u8) -> Option<(usize, usize)> {
284 match magic {
285 LEGACY_MAGIC_CRYPT_START | LEGACY_MAGIC_COMPRESS_CRYPT_START => Some((1 + 4, 0)),
286 LEGACY_NEW_MAGIC_CRYPT_START
287 | LEGACY_NEW_MAGIC_COMPRESS_CRYPT_START
288 | LEGACY_NEW_MAGIC_COMPRESS_CRYPT_START1 => Some((1 + 2 + 1 + 1 + 4, 0)),
289 MAGIC_SYNC_ZLIB_START
290 | MAGIC_SYNC_NO_CRYPT_ZLIB_START
291 | MAGIC_ASYNC_ZLIB_START
292 | MAGIC_ASYNC_NO_CRYPT_ZLIB_START
293 | MAGIC_SYNC_ZSTD_START
294 | MAGIC_SYNC_NO_CRYPT_ZSTD_START
295 | MAGIC_ASYNC_ZSTD_START
296 | MAGIC_ASYNC_NO_CRYPT_ZSTD_START => Some((1 + 2 + 1 + 1 + 4 + 64, 64)),
297 _ => None,
298 }
299}
300
301fn is_good_log_buffer(input: &[u8], offset: usize, count: usize) -> bool {
302 if offset == input.len() {
303 return true;
304 }
305 if count == 0 {
306 return true;
307 }
308 let Ok(header) = parse_header(input, offset) else {
309 return false;
310 };
311 let Some(payload_end) = offset
312 .checked_add(header.header_len)
313 .and_then(|value| value.checked_add(header.len))
314 else {
315 return false;
316 };
317 if payload_end + 1 > input.len() || input[payload_end] != MAGIC_END {
318 return false;
319 }
320 if count <= 1 {
321 return true;
322 }
323 is_good_log_buffer(input, payload_end + 1, count - 1)
324}
325
326fn find_log_start(input: &[u8], count: usize) -> Option<usize> {
327 let mut offset = 0usize;
328 while offset < input.len() {
329 if header_lengths_for_magic(input[offset]).is_some()
330 && is_good_log_buffer(input, offset, count)
331 {
332 return Some(offset);
333 }
334 offset += 1;
335 }
336 None
337}
338
339fn legacy_xor_key(header: &BlockHeader) -> u8 {
340 match header.magic {
341 LEGACY_MAGIC_CRYPT_START | LEGACY_MAGIC_COMPRESS_CRYPT_START => {
342 BASE_KEY ^ (header.len as u8) ^ header.magic
343 }
344 _ => BASE_KEY ^ (header.seq as u8) ^ header.magic,
345 }
346}
347
348fn xor_payload(payload: &[u8], key: u8) -> Vec<u8> {
349 payload.iter().map(|byte| byte ^ key).collect()
350}
351
352fn join_legacy_compressed_segments(payload: &[u8]) -> Vec<u8> {
353 let mut out = Vec::with_capacity(payload.len());
354 let mut offset = 0usize;
355 while offset + 2 <= payload.len() {
356 let len = u16::from_le_bytes([payload[offset], payload[offset + 1]]) as usize;
357 offset += 2;
358 let end = offset.saturating_add(len).min(payload.len());
359 out.extend_from_slice(&payload[offset..end]);
360 offset = end;
361 }
362 out
363}