-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support for ximalaya-pc v4.0.2 generated files
- Loading branch information
Showing
11 changed files
with
321 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
use std::fs::File; | ||
use std::io::{Read, Seek, SeekFrom, Write}; | ||
|
||
use argh::FromArgs; | ||
|
||
use parakeet_crypto::crypto::ximalaya_pc; | ||
|
||
use crate::cli::cli_error::ParakeetCliError; | ||
use crate::cli::logger::CliLogger; | ||
use crate::cli::utils::CliFilePath; | ||
|
||
/// Handle Ximalaya PC encryption/decryption. | ||
#[derive(Debug, Eq, PartialEq, FromArgs)] | ||
#[argh(subcommand, name = "ximalaya-pc")] | ||
pub struct Options { | ||
/// input file name/path | ||
#[argh(option, short = 'i', long = "input")] | ||
input_file: CliFilePath, | ||
|
||
/// output file name/path | ||
#[argh(option, short = 'o', long = "output")] | ||
output_file: CliFilePath, | ||
} | ||
|
||
pub fn handle(args: Options) -> Result<(), ParakeetCliError> { | ||
let log = CliLogger::new("Kugou"); | ||
|
||
let mut src = File::open(args.input_file.path).map_err(ParakeetCliError::SourceIoError)?; | ||
let mut dst = | ||
File::create(args.output_file.path).map_err(ParakeetCliError::DestinationIoError)?; | ||
|
||
let mut buffer = vec![0u8; 1024]; | ||
src.read_exact(&mut buffer) | ||
.map_err(ParakeetCliError::SourceIoError)?; | ||
|
||
let hdr = match ximalaya_pc::Header::from_bytes(&buffer) { | ||
// in case our buffer was too small... | ||
Err(ximalaya_pc::Error::InputTooSmall(n, _)) => { | ||
buffer.resize(n, 0); | ||
|
||
src.seek(SeekFrom::Start(0)) | ||
.and_then(|_| src.read_exact(&mut buffer)) | ||
.map_err(ParakeetCliError::SourceIoError)?; | ||
ximalaya_pc::Header::from_bytes(&buffer)? | ||
} | ||
res => res?, | ||
}; | ||
|
||
log.debug(format!( | ||
"cipher: len(stolen_bytes)={}, cipher_len={}, data_start={}", | ||
hdr.stolen_header_bytes.len(), | ||
hdr.encrypted_header_len, | ||
hdr.data_start_offset, | ||
)); | ||
|
||
// read encrypted part 2 data, and decrypt it | ||
buffer.resize(hdr.encrypted_header_len, 0); | ||
src.seek(SeekFrom::Start(hdr.data_start_offset as u64)) | ||
.and_then(|_| src.read_exact(&mut buffer)) | ||
.map_err(ParakeetCliError::SourceIoError)?; | ||
let decrypted_part_2 = ximalaya_pc::decipher_part_2(&hdr, &buffer)?; | ||
|
||
// write all parts to dst. | ||
dst.write_all(&hdr.stolen_header_bytes) | ||
.and_then(|_| dst.write_all(&decrypted_part_2)) | ||
.and_then(|_| std::io::copy(&mut src, &mut dst)) | ||
.map_err(ParakeetCliError::DestinationIoError)?; | ||
|
||
let bytes_written = dst | ||
.stream_position() | ||
.map_err(ParakeetCliError::DestinationIoError)?; | ||
log.info(format!("decrypt: done, written {} bytes", bytes_written)); | ||
|
||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,3 +4,4 @@ pub mod kugou; | |
pub mod kuwo; | ||
pub mod tencent; | ||
pub mod ximalaya_android; | ||
pub mod ximalaya_pc; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
use crate::crypto::ximalaya_pc::{Error, Header}; | ||
use aes::cipher::block_padding::Pkcs7; | ||
use aes::cipher::{BlockDecryptMut, KeyIvInit}; | ||
use base64::{engine::general_purpose::STANDARD as Base64, DecodeError, Engine as _}; | ||
|
||
type Aes192CbcDec = cbc::Decryptor<aes::Aes192>; | ||
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>; | ||
|
||
const STAGE_1_KEY: &[u8; 32] = include_bytes!("data/stage_1.bin"); | ||
|
||
fn decode_deciphered_content<T: AsRef<[u8]>>(buf: T) -> Result<Vec<u8>, DecodeError> { | ||
Base64.decode(buf) | ||
} | ||
|
||
fn stage_1_decipher<T: AsRef<[u8]>>(buf: T, iv: &[u8; 16]) -> Result<Vec<u8>, Error> { | ||
let mut temp = Vec::from(buf.as_ref()); | ||
|
||
// aes-256-cbc decryption | ||
let buf = Aes256CbcDec::new(STAGE_1_KEY.into(), iv.into()) | ||
.decrypt_padded_mut::<Pkcs7>(&mut temp) | ||
.map_err(Error::Stage1PadError)?; | ||
|
||
decode_deciphered_content(buf).map_err(Error::Stage1CipherDecodeError) | ||
} | ||
|
||
fn stage_2_decipher<T: AsRef<[u8]>>(buf: T, key_iv: &[u8; 24]) -> Result<Vec<u8>, Error> { | ||
let mut temp = Vec::from(buf.as_ref()); | ||
|
||
// aes-192-cbc decryption | ||
let buf = Aes192CbcDec::new(key_iv.into(), key_iv[..16].into()) | ||
.decrypt_padded_mut::<Pkcs7>(&mut temp) | ||
.map_err(Error::Stage2PadError)?; | ||
|
||
decode_deciphered_content(buf).map_err(Error::Stage2CipherDecodeError) | ||
} | ||
|
||
/// Decrypt header | ||
/// `part_2_data` should contain at least `hdr.encrypted_header_len` bytes. | ||
/// Note: | ||
/// - Read & parse header from file | ||
/// - Seek to `hdr.data_start_offset`, read `hdr.encrypted_header_len`. | ||
/// - call `decipher_header` to decrypt. | ||
/// - build the final file: | ||
/// - File header - `hdr.stolen_header_bytes` | ||
/// - Decrypted `part_2_data` after calling this method | ||
/// - Seek to `hdr.data_start_offset + hdr.encrypted_header_len`, copy till EOF. | ||
pub fn decipher_part_2(hdr: &Header, part_2_data: &[u8]) -> Result<Vec<u8>, Error> { | ||
if part_2_data.len() < hdr.encrypted_header_len { | ||
Err(Error::InputTooSmall( | ||
hdr.encrypted_header_len, | ||
part_2_data.len(), | ||
))?; | ||
} | ||
|
||
let buf = &part_2_data[..hdr.encrypted_header_len]; | ||
let buf = stage_1_decipher(buf, &hdr.stage_1_iv)?; | ||
let buf = stage_2_decipher(buf, &hdr.stage_2_key)?; | ||
Ok(buf) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
ximalayaximalayaximalayaximalaya |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
use base64::{engine::general_purpose::STANDARD as Base64, Engine as _}; | ||
use byteorder::{ByteOrder, BE}; | ||
use std::str::FromStr; | ||
|
||
fn parse_safe_sync_u32(v: u32) -> u32 { | ||
let a = v & 0x00_00_00_7f; | ||
let b = (v & 0x00_00_7f_00) >> 1; | ||
let c = (v & 0x00_7f_00_00) >> 2; | ||
let d = (v & 0x7f_00_00_00) >> 3; | ||
a | b | c | d | ||
} | ||
|
||
fn from_utf16_le(data: &[u8]) -> Vec<u8> { | ||
data.chunks(2) | ||
.map_while(|chunk| match chunk[0] { | ||
0 => None, | ||
v => Some(v), | ||
}) | ||
.collect() | ||
} | ||
|
||
pub struct Header { | ||
pub data_start_offset: usize, | ||
pub encrypted_header_len: usize, | ||
pub stage_1_iv: [u8; 16], | ||
/// aes-192, key length = 24-bytes (first 16 byte is also re-used as its iv) | ||
pub stage_2_key: [u8; 24], | ||
pub stolen_header_bytes: Box<[u8]>, | ||
} | ||
|
||
const MAGIC_ID3: [u8; 3] = *b"ID3"; | ||
|
||
impl Header { | ||
pub fn from_bytes<T: AsRef<[u8]>>(data: T) -> Result<Self, super::Error> { | ||
let data = data.as_ref(); | ||
if data.len() < 10 { | ||
Err(super::Error::InputTooSmall(10, data.len()))?; | ||
} | ||
if !data.starts_with(&MAGIC_ID3) { | ||
Err(super::Error::InvalidId3Header)?; | ||
} | ||
let hdr_size = parse_safe_sync_u32(BE::read_u32(&data[6..])); | ||
let data_start_offset = hdr_size as usize + 10; | ||
if data.len() < data_start_offset { | ||
Err(super::Error::InputTooSmall(data_start_offset, data.len()))?; | ||
} | ||
|
||
let mut offset = 10usize; | ||
|
||
let mut result = Self { | ||
data_start_offset, | ||
encrypted_header_len: 0, | ||
stage_1_iv: [0u8; 16], | ||
stage_2_key: [0u8; 24], | ||
stolen_header_bytes: Box::new([]), | ||
}; | ||
|
||
while offset < data_start_offset { | ||
if offset + 10 >= data_start_offset { | ||
Err(super::Error::UnexpectedHeaderEof(offset))?; | ||
} | ||
|
||
let tag_name = &data[offset..offset + 4]; | ||
offset += 4; | ||
let tag_size = BE::read_u32(&data[offset..]) as usize; | ||
offset += 4; | ||
|
||
offset += 2; // flags - not used/ignored | ||
|
||
if offset + tag_size > data_start_offset { | ||
Err(super::Error::UnexpectedHeaderEof(offset))?; | ||
} | ||
|
||
// 01 ff fe ignored - those are encoding marks. All fields are in unicode anyway... | ||
// src: https://web.archive.org/web/2020/https://id3.org/id3v2.3.0#ID3v2_frame_overview | ||
// > If ISO-8859-1 is used this byte should be $00, if Unicode is used it should be $01. | ||
// > Unicode strings must begin with the Unicode BOM ($FF FE or $FE FF) to identify the byte order. | ||
let tag_data = &data[offset + 3..offset + tag_size]; | ||
offset += tag_size; | ||
|
||
match tag_name { | ||
b"TSIZ" => { | ||
let tag_len_str = from_utf16_le(tag_data); | ||
let tag_len_str = String::from_utf8_lossy(tag_len_str.as_slice()); | ||
let header_len = u32::from_str(&tag_len_str) | ||
.map_err(super::Error::DeserializeHeaderValueInt)?; | ||
result.encrypted_header_len = header_len as usize; | ||
} | ||
b"TSRC" | b"TENC" => { | ||
let tag_data = from_utf16_le(tag_data); | ||
let buf_stage1_key = | ||
hex::decode(tag_data).map_err(super::Error::DeserializeHeaderValueHex)?; | ||
if buf_stage1_key.len() != result.stage_1_iv.len() { | ||
Err(super::Error::InvalidData(offset))?; | ||
} | ||
result.stage_1_iv.copy_from_slice(&buf_stage1_key); | ||
} | ||
b"TSSE" => { | ||
let tag_data = from_utf16_le(tag_data); | ||
let stolen_header = Base64 | ||
.decode(tag_data) | ||
.map_err(super::Error::DeserializeHeaderValueBase64)?; | ||
result.stolen_header_bytes = stolen_header.into_boxed_slice(); | ||
} | ||
b"TRCK" => { | ||
let mut tag_data = from_utf16_le(tag_data); | ||
let mut key = *b"123456781234567812345678"; | ||
if tag_data.len() > 24 { | ||
tag_data.drain(..24 - tag_data.len()); | ||
} | ||
let left = key.len() - tag_data.len(); | ||
key[left..].copy_from_slice(&tag_data); | ||
result.stage_2_key = key; | ||
} | ||
_ => { | ||
// ignored | ||
} | ||
} | ||
} | ||
|
||
Ok(result) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
use aes::cipher::block_padding::UnpadError; | ||
use hex::FromHexError; | ||
use std::num::ParseIntError; | ||
use thiserror::Error; | ||
|
||
mod cipher; | ||
mod header; | ||
|
||
pub use cipher::decipher_part_2; | ||
pub use header::Header; | ||
|
||
#[derive(Debug, Error)] | ||
pub enum Error { | ||
#[error("file does not begin with a valid ID3 header")] | ||
InvalidId3Header, | ||
|
||
#[error("Input buffer too small. Expected at least {0} bytes, got {0} bytes.")] | ||
InputTooSmall(usize, usize), | ||
|
||
#[error("Unexpected EOF while parsing header at offset {0}")] | ||
UnexpectedHeaderEof(usize), | ||
|
||
#[error("Could not deserialize an integer: {0}")] | ||
DeserializeHeaderValueInt(ParseIntError), | ||
|
||
#[error("Could not deserialize a hex str to vec: {0}")] | ||
DeserializeHeaderValueHex(FromHexError), | ||
|
||
#[error("Could not deserialize a base64 str to vec: {0}")] | ||
DeserializeHeaderValueBase64(base64::DecodeError), | ||
|
||
#[error("Failed to parse at offset: {0}")] | ||
InvalidData(usize), | ||
|
||
#[error("Failed to decrypt data (stage 1, pkcs#7 padding error): {0}")] | ||
Stage1PadError(UnpadError), | ||
|
||
#[error("Failed to decrypt data (stage 1, b64 decode)")] | ||
Stage1CipherDecodeError(base64::DecodeError), | ||
|
||
#[error("Failed to decrypt data (stage 2, pkcs#7 padding error): {0}")] | ||
Stage2PadError(UnpadError), | ||
|
||
#[error("Failed to decrypt data (stage 2, b64 decode)")] | ||
Stage2CipherDecodeError(base64::DecodeError), | ||
} |