diff --git a/src/ZonyLrcTools.Cli/Infrastructure/MusicDecryption/DecryptionResult.cs b/src/ZonyLrcTools.Cli/Infrastructure/MusicDecryption/DecryptionResult.cs new file mode 100644 index 0000000..67b5c1c --- /dev/null +++ b/src/ZonyLrcTools.Cli/Infrastructure/MusicDecryption/DecryptionResult.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace ZonyLrcTools.Cli.Infrastructure.MusicDecryption +{ + public class DecryptionResult + { + public byte[] Data { get; protected set; } + + public Dictionary ExtensionObjects { get; set; } + + public DecryptionResult(byte[] data) + { + Data = data; + } + } +} \ No newline at end of file diff --git a/src/ZonyLrcTools.Cli/Infrastructure/MusicDecryption/IMusicDecryptor.cs b/src/ZonyLrcTools.Cli/Infrastructure/MusicDecryption/IMusicDecryptor.cs index f16ae63..6e5036e 100644 --- a/src/ZonyLrcTools.Cli/Infrastructure/MusicDecryption/IMusicDecryptor.cs +++ b/src/ZonyLrcTools.Cli/Infrastructure/MusicDecryption/IMusicDecryptor.cs @@ -12,6 +12,6 @@ namespace ZonyLrcTools.Cli.Infrastructure.MusicDecryption /// /// 源加密的歌曲数据。 /// 解密完成的歌曲数据。 - Task Convert(byte[] sourceBytes); + Task ConvertMusic(byte[] sourceBytes); } } \ No newline at end of file diff --git a/src/ZonyLrcTools.Cli/Infrastructure/MusicDecryption/NcmMusicDecryptor.cs b/src/ZonyLrcTools.Cli/Infrastructure/MusicDecryption/NcmMusicDecryptor.cs index ab95522..ae15209 100644 --- a/src/ZonyLrcTools.Cli/Infrastructure/MusicDecryption/NcmMusicDecryptor.cs +++ b/src/ZonyLrcTools.Cli/Infrastructure/MusicDecryption/NcmMusicDecryptor.cs @@ -1,10 +1,11 @@ using System; +using System.Collections.Generic; using System.IO; -using System.Linq; using System.Security.Cryptography; +using System.Text; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; using ZonyLrcTools.Cli.Infrastructure.DependencyInject; -using ZonyLrcTools.Cli.Infrastructure.Exceptions; namespace ZonyLrcTools.Cli.Infrastructure.MusicDecryption { @@ -16,7 +17,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.MusicDecryption protected readonly byte[] AesCoreKey = {0x68, 0x7A, 0x48, 0x52, 0x41, 0x6D, 0x73, 0x6F, 0x35, 0x6B, 0x49, 0x6E, 0x62, 0x61, 0x78, 0x57}; protected readonly byte[] AesModifyKey = {0x23, 0x31, 0x34, 0x6C, 0x6A, 0x6B, 0x5F, 0x21, 0x5C, 0x5D, 0x26, 0x30, 0x55, 0x3C, 0x27, 0x28}; - public Task Convert(byte[] sourceBytes) + public async Task ConvertMusic(byte[] sourceBytes) { var stream = new MemoryStream(sourceBytes); var streamReader = new BinaryReader(stream); @@ -46,7 +47,7 @@ namespace ZonyLrcTools.Cli.Infrastructure.MusicDecryption keyBytes[i] ^= 0x64; } - var deKeyDataBytes = GetBytesByOffset(DecryptAes128Ecb(AesCoreKey, keyBytes), 17); + var coreKeyBytes = GetBytesByOffset(DecryptAes128Ecb(AesCoreKey, keyBytes), 17); var modifyDataBytes = new byte[streamReader.ReadInt32()]; stream.Read(modifyDataBytes); @@ -55,7 +56,25 @@ namespace ZonyLrcTools.Cli.Infrastructure.MusicDecryption modifyDataBytes[i] ^= 0x63; } - throw new System.NotImplementedException(); + var decryptBase64Bytes = Convert.FromBase64String(Encoding.UTF8.GetString(GetBytesByOffset(modifyDataBytes, 22))); + var decryptModifyData = DecryptAes128Ecb(AesModifyKey, decryptBase64Bytes); + + var musicInfoJson = JObject.Parse(Encoding.UTF8.GetString(GetBytesByOffset(decryptModifyData, 6))); + + // CRC 校验 + stream.Seek(4, SeekOrigin.Current); + stream.Seek(5, SeekOrigin.Current); + + GetAlbumImageBytes(stream, streamReader); + + var sBox = BuildKeyBox(coreKeyBytes); + return new DecryptionResult(GetMusicBytes(sBox, stream).ToArray()) + { + ExtensionObjects = new Dictionary + { + {"JSON", musicInfoJson} + } + }; } private byte[] GetBytesByOffset(byte[] srcBytes, int offset = 0) @@ -75,5 +94,91 @@ namespace ZonyLrcTools.Cli.Infrastructure.MusicDecryption return result; } + + /// + /// RC4 加密,生成 KeyBox。 + /// + /// + /// + private byte[] BuildKeyBox(byte[] key) + { + byte[] box = new byte[256]; + for (int i = 0; i < 256; ++i) + { + box[i] = (byte) i; + } + + byte keyLength = (byte) key.Length; + byte c; + byte lastByte = 0; + byte keyOffset = 0; + byte swap; + + for (int i = 0; i < 256; ++i) + { + swap = box[i]; + c = (byte) ((swap + lastByte + key[keyOffset++]) & 0xff); + + if (keyOffset >= keyLength) + { + keyOffset = 0; + } + + box[i] = box[c]; + box[c] = swap; + lastByte = c; + } + + return box; + } + + /// + /// 获得歌曲的专辑图像信息。 + /// + /// 原始文件流。 + /// 二进制读取器。 + private byte[] GetAlbumImageBytes(Stream stream, BinaryReader streamReader) + { + var imgLength = streamReader.ReadInt32(); + + if (imgLength <= 0) + { + return null; + } + + var imgBuffer = streamReader.ReadBytes(imgLength); + + return imgBuffer; + } + + /// + /// 获得歌曲的完整数据。 + /// + /// + /// 原始文件流。 + private MemoryStream GetMusicBytes(byte[] sBox, Stream stream) + { + var n = 0x8000; + var memoryStream = new MemoryStream(); + + while (true) + { + var tb = new byte[n]; + var result = stream.Read(tb); + if (result <= 0) break; + + for (int i = 0; i < n; i++) + { + var j = (byte) ((i + 1) & 0xff); + tb[i] ^= sBox[sBox[j] + sBox[(sBox[j] + j) & 0xff] & 0xff]; + } + + memoryStream.Write(tb); + } + + memoryStream.Flush(); + + return memoryStream; + } } } \ No newline at end of file diff --git a/tests/ZonyLrcTools.Tests/Infrastructure/MusicDecryption/NcmMusicDecryptor_Tests.cs b/tests/ZonyLrcTools.Tests/Infrastructure/MusicDecryption/NcmMusicDecryptor_Tests.cs new file mode 100644 index 0000000..d361f25 --- /dev/null +++ b/tests/ZonyLrcTools.Tests/Infrastructure/MusicDecryption/NcmMusicDecryptor_Tests.cs @@ -0,0 +1,35 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json.Linq; +using Xunit; +using ZonyLrcTools.Cli.Infrastructure.MusicDecryption; + +namespace ZonyLrcTools.Tests.Infrastructure.MusicDecryption +{ + public class NcmMusicDecryptorTests : TestBase + { + [Fact] + public async Task ConvertMusic_Test() + { + var decryptor = ServiceProvider.GetRequiredService(); + + await using var fs = File.Open(Path.Combine(Directory.GetCurrentDirectory(), "MusicFiles", "Loren Gray - Queen.ncm"), FileMode.Open); + using var reader = new BinaryReader(fs); + var response = await decryptor.ConvertMusic(reader.ReadBytes((int) fs.Length)); + + var musicFilePath = Path.Combine(Directory.GetCurrentDirectory(), + "MusicFiles", + $"Loren Gray - Queen.{((JObject) response.ExtensionObjects["JSON"]).SelectToken("$.format").Value()}"); + + if (File.Exists(musicFilePath)) + { + File.Delete(musicFilePath); + } + + await using var musicFileStream = File.Create(musicFilePath); + await musicFileStream.WriteAsync(response.Data); + await musicFileStream.FlushAsync(); + } + } +} \ No newline at end of file diff --git a/tests/ZonyLrcTools.Tests/MusicFiles/Loren Gray - Queen.ncm b/tests/ZonyLrcTools.Tests/MusicFiles/Loren Gray - Queen.ncm new file mode 100644 index 0000000..f54a26b Binary files /dev/null and b/tests/ZonyLrcTools.Tests/MusicFiles/Loren Gray - Queen.ncm differ diff --git a/tests/ZonyLrcTools.Tests/ZonyLrcTools.Tests.csproj b/tests/ZonyLrcTools.Tests/ZonyLrcTools.Tests.csproj index 6725306..ec5ef77 100644 --- a/tests/ZonyLrcTools.Tests/ZonyLrcTools.Tests.csproj +++ b/tests/ZonyLrcTools.Tests/ZonyLrcTools.Tests.csproj @@ -29,6 +29,10 @@ PreserveNewest + + + Always +