feat: Completed the NCM decryption tool class.

完成 NCM 解密工具类。
This commit is contained in:
real-zony 2021-05-31 22:51:10 +08:00
parent fb64e46022
commit def1de8467
6 changed files with 166 additions and 6 deletions

View File

@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace ZonyLrcTools.Cli.Infrastructure.MusicDecryption
{
public class DecryptionResult
{
public byte[] Data { get; protected set; }
public Dictionary<string, object> ExtensionObjects { get; set; }
public DecryptionResult(byte[] data)
{
Data = data;
}
}
}

View File

@ -12,6 +12,6 @@ namespace ZonyLrcTools.Cli.Infrastructure.MusicDecryption
/// </summary>
/// <param name="sourceBytes">源加密的歌曲数据。</param>
/// <returns>解密完成的歌曲数据。</returns>
Task<byte[]> Convert(byte[] sourceBytes);
Task<DecryptionResult> ConvertMusic(byte[] sourceBytes);
}
}

View File

@ -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<byte[]> Convert(byte[] sourceBytes)
public async Task<DecryptionResult> 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<string, object>
{
{"JSON", musicInfoJson}
}
};
}
private byte[] GetBytesByOffset(byte[] srcBytes, int offset = 0)
@ -75,5 +94,91 @@ namespace ZonyLrcTools.Cli.Infrastructure.MusicDecryption
return result;
}
/// <summary>
/// RC4 加密,生成 KeyBox。
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 获得歌曲的专辑图像信息。
/// </summary>
/// <param name="stream">原始文件流。</param>
/// <param name="streamReader">二进制读取器。</param>
private byte[] GetAlbumImageBytes(Stream stream, BinaryReader streamReader)
{
var imgLength = streamReader.ReadInt32();
if (imgLength <= 0)
{
return null;
}
var imgBuffer = streamReader.ReadBytes(imgLength);
return imgBuffer;
}
/// <summary>
/// 获得歌曲的完整数据。
/// </summary>
/// <param name="sBox"></param>
/// <param name="stream">原始文件流。</param>
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;
}
}
}

View File

@ -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<IMusicDecryptor>();
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<string>()}");
if (File.Exists(musicFilePath))
{
File.Delete(musicFilePath);
}
await using var musicFileStream = File.Create(musicFilePath);
await musicFileStream.WriteAsync(response.Data);
await musicFileStream.FlushAsync();
}
}
}

View File

@ -29,6 +29,10 @@
<Content Include="MusicFiles\曾经艺也 - 荀彧(纯音乐版).mp3">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<None Remove="MusicFiles\Loren Gray - Queen.ncm" />
<Content Include="MusicFiles\Loren Gray - Queen.ncm">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>