feat: basic ncm support

This commit is contained in:
鲁树人 2024-09-14 01:33:32 +01:00
parent 7b283a5a14
commit 493ed8bdb5
9 changed files with 541 additions and 0 deletions

View File

@ -8,6 +8,7 @@
<sourceFolder url="file://$MODULE_DIR$/um_crypto/qmc/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/um_cli/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/um_crypto/utils/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/um_crypto/ncm/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />

139
Cargo.lock generated
View File

@ -2,6 +2,17 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "anstream"
version = "0.6.15"
@ -63,6 +74,15 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "block-padding"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
@ -90,6 +110,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "clap"
version = "4.5.17"
@ -146,12 +176,62 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "cpufeatures"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.15"
@ -171,6 +251,16 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"block-padding",
"generic-array",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
@ -229,6 +319,16 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "pretty_assertions"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "proc-macro2"
version = "1.0.86"
@ -247,6 +347,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rhexdump"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a92cbe1a3dd221e3777fef339115d3b85e17a538668e03a0f3ae9c6a98351c7"
[[package]]
name = "same-file"
version = "1.0.6"
@ -315,6 +421,12 @@ dependencies = [
"syn",
]
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "um_cli"
version = "0.1.0"
@ -351,6 +463,21 @@ dependencies = [
"umc_utils",
]
[[package]]
name = "umc_ncm"
version = "0.1.0"
dependencies = [
"aes",
"byteorder",
"cipher",
"crc",
"itertools",
"pretty_assertions",
"rhexdump",
"thiserror",
"umc_utils",
]
[[package]]
name = "umc_qmc"
version = "0.1.0"
@ -383,6 +510,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "walkdir"
version = "2.5.0"
@ -592,3 +725,9 @@ name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "yansi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"

17
um_crypto/ncm/Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "umc_ncm"
version = "0.1.0"
edition = "2021"
[dependencies]
aes = "0.8.4"
byteorder = "1.5.0"
cipher = { version = "0.4.4", features = ["block-padding"] }
crc = "3.2.1"
itertools = "0.13.0"
thiserror = "1.0.63"
umc_utils = { path = "../utils" }
[dev-dependencies]
pretty_assertions = "1"
rhexdump = "0.2.0"

23
um_crypto/ncm/Readme.MD Normal file
View File

@ -0,0 +1,23 @@
# NCM Decoder
## Glossary
- LV: Length-Value Encoding, `length` is u32 in Little-Endian.
## File Format
- Magic: `CTENFDAM`
- NCM Version: `01` (u8)
- App version: `??` (u8)
- ContentKey (LV)
- Metadata (LV)
- CRC32 (of all previous data)
- Cover Block
- Encrypted Audio
### Cover Block
- Length: `u32: frame_len = len(image1 + image2)`
- Length: `u32: img1_len = len(image1)`
- Data: `u8[img1_len]: image1`
- Data: `u8[frame_len - img1_len]: image2` - unknown format

Binary file not shown.

View File

@ -0,0 +1,56 @@
use crate::NetEaseCryptoError;
use aes::cipher::generic_array::GenericArray;
use aes::cipher::KeyInit;
use aes::Aes128Dec;
use cipher::block_padding::Pkcs7;
use cipher::BlockDecryptMut;
use itertools::Itertools;
const CONTENT_KEY: [u8; 0x10] = *b"hzHRAmso5kInbaxW";
/// Decrypt content key
///
/// # Arguments
///
/// * `key`: Encrypted content_key
///
/// returns: Result<Vec<u8, Global>, NetEaseCryptoError>
///
pub fn decrypt<T>(key: T) -> Result<Vec<u8>, NetEaseCryptoError>
where
T: AsRef<[u8]>,
{
let mut data = key.as_ref().iter().map(|&b| b ^ 0x64).collect_vec();
let aes = Aes128Dec::new(&GenericArray::from(CONTENT_KEY));
let content_key = aes
.decrypt_padded_mut::<Pkcs7>(&mut data[..])
.map_err(NetEaseCryptoError::ContentKeyDecryptError)?;
let key = match content_key.strip_prefix(b"neteasecloudmusic") {
None => Err(NetEaseCryptoError::ContentKeyWrongPrefix(
String::from_utf8_lossy(content_key).into(),
))?,
Some(key) => key,
};
Ok(Vec::from(key))
}
#[test]
fn test_decrypt_content_key() {
let enciphered_key = [
0x2C, 0xCE, 0xD5, 0xEB, 0x69, 0xEA, 0xFB, 0x14, 0x55, 0x0D, 0x45, 0xBF, 0x61, 0xDD, 0x17,
0x1D, 0x93, 0x71, 0x47, 0x1E, 0xE1, 0xDD, 0xDA, 0xF4, 0xD5, 0xE8, 0x4F, 0x1C, 0xBA, 0x00,
0x20, 0xC3, 0x02, 0xE9, 0xFE, 0x29, 0x92, 0xE1, 0x81, 0x45, 0x6F, 0x18, 0xC7, 0x2D, 0x11,
0xF2, 0xBC, 0x5B, 0xBC, 0xDC, 0x22, 0x33, 0xF9, 0x68, 0xB4, 0xB0, 0x28, 0x38, 0x3F, 0x63,
0x6C, 0x88, 0x66, 0x35, 0xF9, 0xE7, 0xB1, 0x70, 0x0E, 0xEE, 0x55, 0xAC, 0xB8, 0xED, 0x8B,
0x48, 0x17, 0x25, 0x3A, 0xE6, 0x5E, 0xB5, 0x80, 0x78, 0x8A, 0xCD, 0xDC, 0xE1, 0xEF, 0x3D,
0x30, 0xEC, 0x9C, 0x2A, 0xC6, 0xC7, 0x51, 0xAE, 0x3D, 0x11, 0xB5, 0x64, 0x88, 0x9E, 0xD6,
0x77, 0x66, 0xF6, 0x2B, 0x52, 0x9E, 0xFA, 0xF9, 0x63, 0xF6, 0xDE, 0x27, 0x10, 0x45, 0x82,
0xAC, 0x2D, 0x20, 0x84, 0x95, 0x4C, 0x0F, 0x7A, 0xAE, 0x8B, 0x91, 0x6D, 0x10, 0x2E, 0x63,
0x1C, 0xEA, 0xCA, 0xF9, 0x14, 0x97, 0xD8, 0xB3, 0xE8,
];
let key = decrypt(enciphered_key).expect("Failed to decrypt content");
assert_eq!(key, b"174279197715752960061821572626E7fT49x7dof9OKCgg9cdvhEuezy3iZCL1nFvBFd1T4uSktAJKmwZXsijPbijliionVUXXg9plTbXEclAE9Lb");
}

163
um_crypto/ncm/src/header.rs Normal file
View File

@ -0,0 +1,163 @@
use crate::{content_key, NetEaseCryptoError as Error};
use byteorder::{ByteOrder, LE};
const CRC32: crc::Crc<u32> = crc::Crc::<u32>::new(&crc::CRC_32_ISO_HDLC);
struct NCMFile {
pub ncm_version: u8,
pub client_version: u8,
/// Encrypted content key
pub content_key: Vec<u8>,
/// Encrypted metadata.
pub metadata: Vec<u8>,
/// Cover image 1
pub image1: Option<Vec<u8>>,
/// Cover image 2, format unknown
pub image2: Option<Vec<u8>>,
audio_rc4_key_stream: [u8; 256],
pub audio_data_offset: usize,
}
fn build_audio_rc4_key_stream(enciphered_content_key: &[u8]) -> Result<[u8; 256], Error> {
let key = content_key::decrypt(enciphered_content_key)?;
let mut s = [0u8; 256];
s.iter_mut().enumerate().for_each(|(i, b)| *b = i as u8);
{
let mut j = 0u8;
for (i, &k) in (0..256).zip(key.iter().cycle()) {
j = j.wrapping_add(s[i]).wrapping_add(k);
s.swap(i, j as usize);
}
}
let mut key = [0u8; 256];
for (i, k) in key.iter_mut().enumerate() {
let j = s[i].wrapping_add(i as u8);
let idx = s[i].wrapping_add(s[j as usize]);
*k = s[idx as usize];
}
Ok(key)
}
impl NCMFile {
pub fn new<T>(header: T) -> Result<Self, Error>
where
T: AsRef<[u8]>,
{
let header = header.as_ref();
if header.len() < 14 {
Err(Error::HeaderTooSmall(14 - header.len()))?;
}
if !header.starts_with(b"CTENFDAM") {
Err(Error::NotNCMFile)?;
}
let ncm_version = header[8];
let client_version = header[9];
let content_key_len = LE::read_u32(&header[10..14]) as usize;
let offset = 14;
if header.len() < offset + content_key_len + 4 {
Err(Error::HeaderTooSmall(offset + content_key_len + 4))?;
}
let content_key = &header[offset..offset + content_key_len];
let offset = offset + content_key_len;
let metadata_len = LE::read_u32(&header[offset..offset + 4]) as usize;
let offset = offset + 4;
// 9: crc32 + cover_version + frame_size
if header.len() < offset + metadata_len + 9 {
Err(Error::HeaderTooSmall(offset + metadata_len + 9))?;
}
let metadata = &header[offset..offset + metadata_len];
let offset = offset + metadata_len;
let expected_crc32 = LE::read_u32(&header[offset..offset + 4]);
let actual_checksum = CRC32.checksum(&header[..offset]);
if actual_checksum != expected_crc32 {
Err(Error::ChecksumInvalid {
expected: expected_crc32,
actual: actual_checksum,
})?;
}
let offset = offset + 4;
let cover_version = header[offset];
match cover_version {
1 => {}
v => Err(Error::UnsupportedCoverImageVersion(v))?,
};
let offset = offset + 1;
let cover_frame_len = LE::read_u32(&header[offset..offset + 4]) as usize;
if header.len() < offset + cover_frame_len + 4 {
Err(Error::HeaderTooSmall(offset + cover_frame_len + 4))?;
}
let offset = offset + 4;
let image1_len = LE::read_u32(&header[offset..offset + 4]) as usize;
if image1_len > cover_frame_len {
Err(Error::InvalidCoverImage2Size {
frame_size: cover_frame_len,
image1_size: image1_len,
})?;
}
let offset = offset + 4;
let image2_len = cover_frame_len - image1_len;
let image1 = match image1_len {
0 => None,
len => Some(Vec::from(&header[offset..offset + len])),
};
let offset = offset + image1_len;
let image2 = match image2_len {
0 => None,
len => Some(Vec::from(&header[offset..offset + len])),
};
let offset = offset + image2_len;
Ok(Self {
ncm_version,
client_version,
content_key: Vec::from(content_key),
metadata: Vec::from(metadata),
image1,
image2,
audio_rc4_key_stream: build_audio_rc4_key_stream(content_key)?,
audio_data_offset: offset,
})
}
}
#[test]
fn test_load_ncm() -> Result<(), Error> {
let ncm_header = include_bytes!("__fixture__/ncm_test1.bin");
let ncm = NCMFile::new(ncm_header)?;
let sbox = [
0x08, 0x67, 0x20, 0xF0, 0x5C, 0xAC, 0xAF, 0x1B, 0x74, 0x0D, 0x26, 0x40, 0xBE, 0x85, 0x61,
0x45, 0x0D, 0xA8, 0x69, 0xAE, 0x78, 0xEC, 0xB8, 0x14, 0x79, 0x53, 0xC4, 0x74, 0xF2, 0x9F,
0xE1, 0x18, 0xE2, 0xF9, 0xC0, 0x7D, 0x1E, 0x5A, 0xBA, 0x4A, 0xB3, 0x11, 0x36, 0xD9, 0xAA,
0xD2, 0x13, 0xEE, 0x92, 0x45, 0x4B, 0x57, 0xE6, 0xF1, 0x13, 0x90, 0xE9, 0xE5, 0x2F, 0xBE,
0x6E, 0x16, 0x01, 0xBF, 0x10, 0x81, 0x5C, 0x68, 0x1C, 0xE3, 0xEB, 0xDF, 0x5A, 0xC7, 0xC3,
0x3F, 0x9C, 0xD7, 0x28, 0x3A, 0x81, 0x2B, 0x1F, 0xF3, 0x6F, 0x27, 0x15, 0x7E, 0x53, 0xD3,
0xF0, 0xFA, 0x50, 0x9F, 0xC7, 0xF0, 0x7F, 0xA6, 0xA7, 0x24, 0x80, 0x87, 0x39, 0x55, 0xA5,
0x0B, 0x2B, 0x8B, 0x00, 0x19, 0x82, 0xA8, 0x40, 0xA5, 0x77, 0x9C, 0x93, 0x3B, 0xEE, 0x27,
0x70, 0x0F, 0xA2, 0xEB, 0xA5, 0x6F, 0x58, 0xFB, 0xEB, 0x57, 0x1B, 0xC3, 0x61, 0x10, 0x6D,
0x95, 0x2C, 0xAA, 0xC6, 0x58, 0x88, 0xD5, 0x9E, 0x33, 0x9A, 0x5A, 0xD1, 0xB5, 0xD2, 0x17,
0x44, 0xD6, 0xDB, 0xAB, 0xED, 0x7C, 0xFC, 0x5C, 0x37, 0xB9, 0x16, 0xB8, 0xA5, 0x77, 0xFB,
0x58, 0x35, 0x36, 0xE7, 0x51, 0xD0, 0x03, 0xBB, 0x2A, 0x88, 0x24, 0x84, 0x1D, 0x28, 0xB1,
0xE1, 0xA0, 0xA5, 0xA0, 0xDD, 0x15, 0x66, 0xBB, 0x4F, 0x7E, 0xBC, 0x99, 0x76, 0x1A, 0xD2,
0xEF, 0xBE, 0xD9, 0x79, 0xA0, 0x33, 0xD5, 0x96, 0x6C, 0xD9, 0xEF, 0x42, 0x7E, 0x11, 0x45,
0x1F, 0x99, 0x96, 0x1A, 0x90, 0x0C, 0x75, 0xBC, 0x01, 0xE2, 0xC8, 0xEF, 0x16, 0x98, 0x9A,
0x09, 0xEB, 0xD8, 0xA1, 0xEA, 0x62, 0xE7, 0x92, 0x20, 0x8F, 0x6F, 0x72, 0xDE, 0x14, 0xFE,
0xBF, 0xCD, 0xBA, 0x97, 0xDE, 0xA3, 0x3B, 0x67, 0xD4, 0x0B, 0xF4, 0xD3, 0x97, 0x25, 0xBA,
0xCA,
];
assert_eq!(ncm.audio_rc4_key_stream, sbox);
assert_eq!(ncm.image1, Some(b"img#1".to_vec()));
assert_eq!(ncm.image2, Some(b"IMAGE#2".to_vec()));
Ok(())
}

37
um_crypto/ncm/src/lib.rs Normal file
View File

@ -0,0 +1,37 @@
pub mod content_key;
pub mod header;
pub mod metadata;
use cipher::block_padding::UnpadError;
use thiserror::Error;
use umc_utils::base64;
#[derive(Error, Debug)]
pub enum NetEaseCryptoError {
#[error("Header need at least {0} more bytes")]
HeaderTooSmall(usize),
#[error("Not a NCM file")]
NotNCMFile,
#[error("Invalid NCM checksum. Expected {expected:08x}, actual: {expected:08x}")]
ChecksumInvalid { expected: u32, actual: u32 },
#[error("Unsupported cover image version: {0}")]
UnsupportedCoverImageVersion(u8),
#[error("Cover image: Frame size is less than image 1. frame_size:{frame_size}, image1_size:{image1_size}")]
InvalidCoverImage2Size {
frame_size: usize,
image1_size: usize,
},
#[error("ContentKey: AES PKCS#7 Decode Error")]
ContentKeyDecryptError(UnpadError),
#[error("ContentKey: Invalid key prefix: {0}")]
ContentKeyWrongPrefix(String),
#[error("Metadata: Invalid prefix while decoding: {0}")]
MetadataWrongPrefix(String),
#[error("Metadata: AES PKCS#7 Decode Error")]
MetadataDecryptError(UnpadError),
#[error("Metadata: Decode metadata failed: {0}")]
MetadataDecodeError(base64::DecodeError),
}

View File

@ -0,0 +1,105 @@
use crate::NetEaseCryptoError;
use aes::Aes128Dec;
use cipher::block_padding::Pkcs7;
use cipher::generic_array::GenericArray;
use cipher::{BlockDecrypt, KeyInit};
use itertools::Itertools;
use umc_utils::base64;
const METADATA_KEY: [u8; 0x10] = *b"#14ljk_!\\]&0U<'(";
/// Decrypt metadata
///
/// # Arguments
///
/// * `key`: Encrypted metadata
///
/// returns: Result<Vec<u8, Global>, NetEaseCryptoError>
pub fn decrypt<T>(enciphered_metadata: T) -> Result<Vec<u8>, NetEaseCryptoError>
where
T: AsRef<[u8]>,
{
let data = enciphered_metadata
.as_ref()
.iter()
.map(|&b| b ^ 0x63)
.collect_vec();
let data = match data.strip_prefix(b"163 key(Don't modify):") {
None => Err(NetEaseCryptoError::MetadataWrongPrefix(
String::from_utf8_lossy(&data[..]).to_string(),
))?,
Some(data) => data,
};
let mut data = base64::decode(data).map_err(NetEaseCryptoError::MetadataDecodeError)?;
let aes = Aes128Dec::new(&GenericArray::from(METADATA_KEY));
let metadata = aes
.decrypt_padded::<Pkcs7>(&mut data[..])
.map_err(NetEaseCryptoError::MetadataDecryptError)?;
Ok(Vec::from(metadata))
}
#[test]
fn test_decrypt_metadata() -> Result<(), NetEaseCryptoError> {
let enciphered_metadata = [
0x52, 0x55, 0x50, 0x43, 0x08, 0x06, 0x1A, 0x4B, 0x27, 0x0C, 0x0D, 0x44, 0x17, 0x43, 0x0E,
0x0C, 0x07, 0x0A, 0x05, 0x1A, 0x4A, 0x59, 0x2F, 0x55, 0x57, 0x25, 0x36, 0x50, 0x34, 0x57,
0x3A, 0x1B, 0x3B, 0x50, 0x39, 0x25, 0x37, 0x0E, 0x01, 0x39, 0x48, 0x5B, 0x4C, 0x05, 0x09,
0x07, 0x2B, 0x55, 0x22, 0x00, 0x48, 0x31, 0x32, 0x4C, 0x17, 0x07, 0x0D, 0x24, 0x1A, 0x39,
0x11, 0x26, 0x5A, 0x15, 0x29, 0x24, 0x50, 0x02, 0x07, 0x20, 0x56, 0x2D, 0x33, 0x5A, 0x0C,
0x10, 0x29, 0x26, 0x22, 0x28, 0x4C, 0x1A, 0x0D, 0x0B, 0x06, 0x28, 0x0D, 0x2C, 0x1B, 0x09,
0x2E, 0x0B, 0x2B, 0x28, 0x02, 0x13, 0x0F, 0x2F, 0x2C, 0x0E, 0x25, 0x1B, 0x4C, 0x2B, 0x52,
0x33, 0x36, 0x09, 0x37, 0x2D, 0x0B, 0x25, 0x27, 0x2C, 0x0C, 0x0D, 0x02, 0x51, 0x35, 0x5A,
0x1A, 0x35, 0x2D, 0x2F, 0x3B, 0x54, 0x2A, 0x0D, 0x4C, 0x2B, 0x16, 0x1B, 0x0C, 0x26, 0x21,
0x0B, 0x52, 0x25, 0x52, 0x36, 0x5B, 0x51, 0x50, 0x56, 0x01, 0x0A, 0x19, 0x14, 0x5B, 0x57,
0x32, 0x07, 0x06, 0x50, 0x25, 0x55, 0x54, 0x53, 0x30, 0x2C, 0x32, 0x52, 0x15, 0x00, 0x50,
0x37, 0x39, 0x28, 0x0E, 0x15, 0x0F, 0x36, 0x48, 0x11, 0x24, 0x33, 0x56, 0x07, 0x22, 0x20,
0x29, 0x14, 0x28, 0x06, 0x16, 0x01, 0x54, 0x48, 0x0B, 0x17, 0x20, 0x25, 0x36, 0x02, 0x01,
0x16, 0x07, 0x27, 0x16, 0x0C, 0x37, 0x20, 0x2A, 0x24, 0x21, 0x22, 0x50, 0x56, 0x0E, 0x34,
0x52, 0x37, 0x14, 0x29, 0x10, 0x32, 0x20, 0x36, 0x2D, 0x52, 0x21, 0x02, 0x09, 0x3B, 0x0B,
0x01, 0x17, 0x00, 0x33, 0x36, 0x0D, 0x01, 0x21, 0x2B, 0x17, 0x4C, 0x39, 0x02, 0x33, 0x51,
0x02, 0x20, 0x07, 0x37, 0x4C, 0x5A, 0x25, 0x39, 0x09, 0x51, 0x25, 0x50, 0x54, 0x4C, 0x19,
0x32, 0x37, 0x08, 0x06, 0x32, 0x55, 0x1B, 0x53, 0x0B, 0x0B, 0x4C, 0x08, 0x16, 0x33, 0x54,
0x27, 0x11, 0x02, 0x1A, 0x30, 0x28, 0x57, 0x25, 0x50, 0x3B, 0x2E, 0x28, 0x34, 0x51, 0x3B,
0x36, 0x11, 0x53, 0x12, 0x56, 0x09, 0x21, 0x2B, 0x2C, 0x14, 0x2D, 0x24, 0x5A, 0x02, 0x53,
0x16, 0x21, 0x29, 0x16, 0x02, 0x53, 0x02, 0x2A, 0x31, 0x2D, 0x31, 0x56, 0x3B, 0x17, 0x51,
0x52, 0x0F, 0x56, 0x50, 0x56, 0x00, 0x17, 0x57, 0x2F, 0x08, 0x1A, 0x52, 0x25, 0x12, 0x2D,
0x48, 0x05, 0x02, 0x37, 0x0F, 0x36, 0x30, 0x05, 0x14, 0x5A, 0x37, 0x5A, 0x50, 0x55, 0x16,
0x09, 0x53, 0x3B, 0x1B, 0x24, 0x1A, 0x1A, 0x21, 0x50, 0x06, 0x37, 0x2E, 0x57, 0x09, 0x39,
0x17, 0x13, 0x26, 0x31, 0x04, 0x52, 0x50, 0x55, 0x10, 0x53, 0x52, 0x57, 0x02, 0x04, 0x02,
0x2F, 0x01, 0x5A, 0x02, 0x2A, 0x2F, 0x19, 0x4C, 0x0C, 0x55, 0x00, 0x50, 0x2D, 0x29, 0x06,
0x16, 0x11, 0x20, 0x0D, 0x01, 0x53, 0x22, 0x32, 0x1B, 0x34, 0x00, 0x0A, 0x06, 0x02, 0x15,
0x0F, 0x25, 0x5A, 0x1A, 0x22, 0x11, 0x0E, 0x21, 0x32, 0x31, 0x22, 0x39, 0x56, 0x5A, 0x09,
0x16, 0x35, 0x16, 0x30, 0x29, 0x0F, 0x30, 0x27, 0x2C, 0x55, 0x52, 0x12, 0x48, 0x16, 0x0F,
0x0C, 0x36, 0x0C, 0x25, 0x27, 0x28, 0x35, 0x0A, 0x1A, 0x06, 0x09, 0x16, 0x39, 0x1B, 0x08,
0x10, 0x27, 0x12, 0x2C, 0x2C, 0x5B, 0x48, 0x01, 0x48, 0x33, 0x48, 0x0E, 0x1B, 0x54, 0x1B,
0x02, 0x36, 0x25, 0x20, 0x48, 0x31, 0x0B, 0x06, 0x0D, 0x15, 0x11, 0x17, 0x01, 0x24, 0x20,
0x27, 0x31, 0x30, 0x22, 0x0F, 0x22, 0x08, 0x17, 0x0F, 0x13, 0x0C, 0x0C, 0x1A, 0x12, 0x12,
0x51, 0x4C, 0x3B, 0x53, 0x3B, 0x37, 0x39, 0x16, 0x25, 0x05, 0x54, 0x10, 0x32, 0x0D, 0x48,
0x0C, 0x50, 0x5B, 0x20, 0x0B, 0x19, 0x22, 0x19, 0x0D, 0x0B, 0x28, 0x2C, 0x0B, 0x05, 0x3B,
0x51, 0x09, 0x54, 0x0A, 0x06, 0x0F, 0x57, 0x08, 0x51, 0x50, 0x24, 0x29, 0x16, 0x57, 0x33,
0x36, 0x15, 0x04, 0x50, 0x5B, 0x2F, 0x51, 0x56, 0x12, 0x09, 0x2F, 0x27, 0x30, 0x51, 0x2B,
0x37, 0x57, 0x24, 0x35, 0x5B, 0x0C, 0x0E, 0x0D, 0x0D, 0x53, 0x2A, 0x25, 0x06, 0x33, 0x22,
0x29, 0x51, 0x24, 0x08, 0x51, 0x3A, 0x06, 0x0E, 0x16, 0x31, 0x36, 0x2B, 0x10, 0x34, 0x30,
0x08, 0x0A, 0x2F, 0x19, 0x32, 0x5A, 0x04, 0x39, 0x52, 0x36, 0x1A, 0x26, 0x14, 0x0E, 0x0B,
0x2A, 0x4C, 0x39, 0x56, 0x1A, 0x01, 0x2F, 0x14, 0x33, 0x33, 0x56, 0x16, 0x25, 0x29, 0x33,
0x0E, 0x0B, 0x2D, 0x1A, 0x11, 0x29, 0x08, 0x15, 0x02, 0x2B, 0x4C, 0x01, 0x0C, 0x07, 0x0F,
0x15, 0x4C, 0x36, 0x36, 0x09, 0x4C, 0x13, 0x53, 0x11, 0x11, 0x12, 0x02, 0x0F, 0x11, 0x56,
0x2B, 0x5B, 0x21, 0x29, 0x0B, 0x51, 0x09, 0x02, 0x3B, 0x11, 0x02, 0x24, 0x16, 0x12, 0x04,
0x0A, 0x37, 0x0B, 0x22, 0x5A, 0x0B, 0x02, 0x0A, 0x02, 0x36, 0x36, 0x2A, 0x29, 0x1A, 0x14,
0x14, 0x13, 0x1B, 0x2C, 0x11, 0x31, 0x2A, 0x4C, 0x28, 0x11, 0x2B, 0x1B, 0x30, 0x51, 0x35,
0x11, 0x53, 0x0D, 0x09, 0x0F, 0x39, 0x0F, 0x25, 0x17, 0x02, 0x5B, 0x1A, 0x39, 0x26, 0x5B,
0x51, 0x3B, 0x2E, 0x04, 0x2E, 0x21, 0x36, 0x04, 0x3B, 0x11, 0x56, 0x0A, 0x2E, 0x2D, 0x1A,
0x27, 0x21, 0x10, 0x07, 0x53, 0x11, 0x37, 0x07, 0x2F, 0x51, 0x57, 0x2B, 0x24, 0x57, 0x01,
0x53, 0x35, 0x52, 0x0F, 0x2F, 0x2D, 0x48, 0x52, 0x56, 0x2B, 0x00, 0x17, 0x1A, 0x39, 0x00,
0x12, 0x09, 0x02, 0x07, 0x02, 0x52, 0x2C, 0x20, 0x4C, 0x39, 0x4C, 0x48, 0x50, 0x0D, 0x0F,
0x15, 0x4C, 0x00, 0x2B, 0x0F, 0x37, 0x48, 0x19, 0x17, 0x32, 0x35, 0x31, 0x28, 0x0B, 0x0D,
0x2D, 0x32, 0x10, 0x32, 0x56, 0x33, 0x28, 0x3B, 0x2B, 0x55, 0x12, 0x04, 0x07, 0x50, 0x24,
0x00, 0x08, 0x01, 0x10, 0x26, 0x30, 0x2C, 0x34, 0x10, 0x21, 0x0D, 0x25, 0x02, 0x2E, 0x53,
0x11, 0x2B, 0x19, 0x36, 0x31, 0x2E, 0x5A, 0x52, 0x33, 0x35, 0x20, 0x14, 0x53, 0x30, 0x04,
0x05, 0x08, 0x34, 0x13, 0x02, 0x54, 0x22, 0x05, 0x5A, 0x01, 0x20, 0x13, 0x14, 0x39, 0x2A,
0x25, 0x1B, 0x37, 0x48, 0x57, 0x53, 0x2A, 0x22, 0x3A, 0x00, 0x34, 0x53, 0x24, 0x12,
];
let key = decrypt(enciphered_metadata)?;
assert_eq!(&key[..6], b"music:");
Ok(())
}