Skip to content

Commit

Permalink
Merge pull request #7 from nukemiko/2.x
Browse files Browse the repository at this point in the history
正式版更新:2.1.0
  • Loading branch information
nukemiko authored Feb 10, 2023
2 parents aa1f053 + dc9a967 commit 86ce995
Show file tree
Hide file tree
Showing 10 changed files with 376 additions and 784 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
## 变更记录

### 版本 2.1.0

- 减少了一些重复代码的使用,删除了大量不再使用的代码
- 优化了判断是否为 QMCv1 文件的逻辑、QMCv2 文件的主密钥探测逻辑
- `libtakiyasha.qmc.QMCv2``open()``save()` 现在可接受多个混淆密钥,通过关键字参数 `garble_keys` 在需要时传入。
- **因此,上述方法中原来的关键字参数 `garble_key1``garble_key2` 已经被干掉了,请及时修改你的工具链。**
- 如果提供此参数,需要提供一个产生至少一个混淆密钥(类字节对象)的可迭代对象(例如列表),且混淆密钥的顺序必须正确。

[这里](https://github.com/nukemiko/libtakiyasha/compare/2.1.0rc2...2.1.0)查看更详细的变更记录。

### 版本 2.0.1 至 2.1.0rc2

[这里](https://github.com/nukemiko/libtakiyasha/compare/2.0.1...2.1.0rc2)查看更详细的变更记录。
129 changes: 31 additions & 98 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ LibTakiyasha 是一个 Python 音频加密/解密工具库(当然也可用于

**本项目是以学习和技术研究的初衷创建的,修改、再分发时请遵循 [License](LICENSE)**

本项目的设计灵感,以及部分解密方案,来源于同类项目
本项目的设计灵感,以及部分解密方案,来源于<span id='similar-projects'>同类项目</span>

- [Unlock Music Project - CLI Edition](https://git.unlock-music.dev/um/cli)
- [parakeet-rs/libparakeet](https://github.com/parakeet-rs/libparakeet)
Expand All @@ -19,37 +19,39 @@ LibTakiyasha 是一个 Python 音频加密/解密工具库(当然也可用于

---

## 新变化?

请参阅[变更记录](CHANGELOG.md)

(如果你是在 PyPI 上浏览本项目,它可能会出现在页面的底部,[点按此处跳转](#变更记录)。)

## 特性

- 使用纯 Python 代码编写
- **兼容 Python 3.8 及后续版本**,兼容多种 Python 解释器实现(见下文 [#性能测试](#性能测试)
- **兼容 Python 3.8 及后续版本**,兼容多种 Python 解释器实现
- 可在[此处](https://github.com/nukemiko/libtakiyasha/wiki/%E6%80%A7%E8%83%BD%E8%A1%A8%E7%8E%B0)查看具体兼容哪些实现
- 易于阅读,方便 Python 爱好者学习
- (包括依赖库)无任何 C/C++ 扩展模块,跨平台性强
- 支持四种加密文件:
- 网易云音乐加密文件 `.ncm`
- QQ 音乐加密文件 QMCv1 `.qmc[0-9]``.qmcflac``.qmcogg``.qmcra`
- QQ 音乐加密文件 QMCv2 `.mflac[0-9]``.mgg[0-9]`
- 酷狗音乐加密文件 KGM/VPR `.kgm``.vpr`
- 不支持创建新加密文件
- 酷我音乐加密文件 `.kwm`
- 更多信息,请参见[此处](https://github.com/nukemiko/libtakiyasha/wiki/%E6%94%AF%E6%8C%81%E7%9A%84%E6%A0%BC%E5%BC%8F%E4%BB%A5%E5%8F%8A%E6%89%80%E9%9C%80%E7%9A%84%E5%8F%82%E6%95%B0)

### 性能测试
### 性能表现

由于 Python 语言自身原因,LibTakiyasha 相较于同类项目,运行速度较慢。因此我们使用不同解释器实现,对常用操作做了一些性能测试:
参见[此处](https://github.com/nukemiko/libtakiyasha/wiki/%E6%80%A7%E8%83%BD%E8%A1%A8%E7%8E%B0)

| 操作 | 测试大小 | Python 3.10.9 (CPython) | Python 3.8.12 (Pyston 2.3.5) | Python 3.9.16 (PyPy 7.3.11) |
| :------------: | :------: | :---------------------: | :--------------------------: | :-------------------------: |
| NCM 加密 | 36.8 MiB | 4.159s | 2.159s | 1.366s |
| NCM 解密 | 36.8 MiB | 4.393s | 2.360s | 1.480s |
| QMCv1 加密 | 36.8 MiB | 3.841s | 2.116s | 1.594s |
| QMCv1 解密 | 36.8 MiB | 3.813s | 2.331s | 1.406s |
| QMCv2 掩码加密 | 36.8 MiB | 4.065s | 2.201s | 1.727s |
| QMCv2 掩码解密 | 36.8 MiB | 3.990s | 2.200s | 1.848s |
| QMCv2 RC4 加密 | 36.8 MiB | 12.820s | 5.596s | 2.717s |
| QMCv2 RC4 解密 | 36.8 MiB | 12.588s | 5.913s | 2.552s |
| KGM 解密 | 64.4 MiB | 49.014s | 22.053s | 8.376s |
| VPR 解密 | 87.9 MiB | 70.030s | 32.252s | 11.902s |

仅在你对速度有要求时,可以考虑在调用 LibTakiyasha 时使用 PyPy/Pyston 解释器。
## 安装

一般情况下,建议使用官方解释器实现(CPython)。
可用的最新版本:2.1.0,[GitHub 发布页面](https://github.com/nukemiko/libtakiyasha/releases/tag/2.1.0)[PyPI](https://pypi.org/project/libtakiyasha/2.1.0/)

## 安装
### 安装方式

可用的最新版本:2.1.0rc2,可前往[发布页面](https://github.com/nukemiko/libtakiyasha/releases/tag/2.1.0rc2)[PyPI](https://pypi.org/project/libtakiyasha/2.1.0rc2/) 下载。
- 使用 `pip`,通过 PyPI 安装最新版本:`python -m pip install -U libtakiyasha`

如果你要下载其他版本:

Expand All @@ -61,8 +63,14 @@ LibTakiyasha 是一个 Python 音频加密/解密工具库(当然也可用于

LibTakiyasha 依赖以下包,均可从 PyPI 获取:

- [pyaes](https://pypi.org/project/pyaes/)
- [mutagen](https://pypi.org/project/mutagen/)
- [pyaes](https://pypi.org/project/pyaes/) - 用于加解密 NCM 文件内嵌的主密钥和元数据
- [mutagen](https://pypi.org/project/mutagen/) - 用于以 `mutagen` 可接受的形式导出 NCM 文件内嵌的元数据

## 如何使用?

[这里](https://github.com/nukemiko/libtakiyasha/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%E5%8F%8A%E7%A4%BA%E4%BE%8B)可以找到使用方法和示例。

同时,在[本项目的 Wiki 主页](https://github.com/nukemiko/libtakiyasha/wiki)可以找到其他一些可能对你有用的东西。

## 常见问题

Expand All @@ -73,78 +81,3 @@ LibTakiyasha 依赖以下包,均可从 PyPI 获取:
- LibTakiyasha 是一个加解密库,当然需要为用户提供自定义密钥的权利
- 为了保护本项目不受美国<ruby>数字千年版权法<rt>Digital Millennium Copyright Act</rt></ruby>(DMCA)影响,避免仓库被误杀
- 因此,本仓库所有 1.x 及更早版本的提交和发布版本都已删除。

> 如何使用?
当你 `import libtakiyasha` 时,`libtakiyasha` 下有四个子模块 `ncm``qmc``kgmvpr``kwm` 会被自动导入。这些子模块下各有一个加密文件对象类(`qmc` 除外,有两个),和一个 `probe` 开头的探测函数(`qmc` 除外,有三个),用于确认目标文件是否被该模块支持:

| 模块 | 加密文件对象类 | 探测函数 |
| :-------------------: | :----------------: | :-----------------------------------------------: |
| `libtakiyasha.ncm` | `NCM` | `probe_ncm()` |
| `libtakiyasha.qmc` | `QMCv1``QMCv2` | `probe_qmc()``probe_qmcv1()``probe_qmcv2()` |
| `libtakiyasha.kgmvpr` | `KGMorVPR` | `probe_kgmvpr()` |
| `libtakiyasha.kwm` | `KWM` | `probe_kwm()` |

每个探测函数都会返回一个内含两个元素的元组:

- 第一个元素为文件路径或文件对象,取决于探测函数收到的参数;
- 在探测到受支持的文件时,第二个元素为文件的信息,否则为 `None`

每一个加密文件对象都可以按照普通文件对象对待(拥有 `read()``write()``seek()` 等方法),也拥有一个 `save()` 方法,以便将该加密文件对象保存到文件。

`libtakiyasha.ncm.NCM` 为例,以下是简单的使用示例:

- 要想打开外部加密文件,或新建空加密文件,使用对应加密文件对象类的构造器方法 `open()``new()`

```pycon
>>> # 打开外部加密文件
>>> ncmfile_from_open = libtakiyasha.ncm.NCM.open('/path/to/ncmmfile.ncm', core_key=..., tag_key=...)
>>> ncmfile_from_open
<libtakiyasha.ncm.NCM at 0x7f26c44e5080, cipher <libtakiyasha.stdciphers.ARC4 object at 0x7f26c4ef1270>, source '/path/to/ncmfile.ncm'>
>>> # 新建空加密文件对象
>>> ncmfile_new = libtakiyasha.ncm.NCM.new()
>>> ncmfile_new
<libtakiyasha.stdciphers.ARC4 object at 0x7f26c51214b0>
>>>
```

- 从加密文件中读取和写入数据:

```pycon
>>> # 读取 16 字节
>>> ncmfile_from_open.read(16)
b'fLaC\\x00\\x00\\x00"\\x12\\x00\\x12\\x00\\x00\\x07)\\x00'
>>> # 读取一行数据,直到下一个换行符 \n
>>> ncmfile_from_open.readline()
b'\xc4B\xf0\x00\xb6\xe14A\x86nz.\x97\xa8\xe3\xbe\x1d\xb7\xb02?u&\x03\x00\t\x90\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x12\x00\x00\x00\x00\x00\x00\x01V\x00\x00\x00\x00\x00\x00\x01\xd6`\x12\x00\x00\x00\x00\x00\x00\x02\xac\x00\x00\x00\x00\x00\x00\x04\x92B\x12\x00\x00\x00\x00\x00\x00\x04\x02\x00\x00\x00\x00\x00\x00\x07\x0f\xb2\x12\x00\x00\x00\x00\x00\x00\x05X\x00\x00\x00\x00\x00\x00\t\xd4\x8c\x12\x00\x00\x00\x00\x00\x00\x06\xae\x00\x00\x00\x00\x00\x00\x0c\xa3\xb6\x12\x00\x00\x00\x00\x00\x00\x08\x04\x00\x00\x00\x00\x00\x00\x0f|\x90\x12\x00\x00\x00\x00\x00\x00\tZ\x00\x00\x00\x00\x00\x00\x12^T\x12\x00\x00\x00\x00\x00\x00\n'
>>>
>>> # 使用 for 循环按照固定大小迭代加密文件对象
>>> ncmfile_from_open.seek(0, 0)
0
>>> for blk in ncmfile_from_open:
... print(len(blk))
...
8192
8192
8192
8192
8192
8192
[...]
>>> # 向加密文件对象写入数据
>>> ncmfile_from_open.seek(0, 2)
36137109
>>> ncmfile_from_open.write(b'Now I writing something...')
26
>>>
```

- 保存加密文件对象到文件:

```pycon
>>> # 如果该 NCM 对象不是从文件打开的,还需要 filething 参数
>>> ncmfile_from_open.save(core_key=..., tag_key=...)
>>>
```

有关每个加密文件的操作示例,请使用 `help()` 查看对应加密文件对象类的文档。
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ where = ["src"]

[tool.setuptools.dynamic]
dependencies = { file = ["requirements.txt"] }
readme = { file = ["README.md"], content-type = 'text/markdown' }
readme = { file = ["README.md", "CHANGELOG.md"], content-type = 'text/markdown' }
version = { file = "src/libtakiyasha/VERSION" }
2 changes: 1 addition & 1 deletion src/libtakiyasha/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.1.0rc2
2.1.0
118 changes: 62 additions & 56 deletions src/libtakiyasha/kgmvpr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@

import warnings
from pathlib import Path
from typing import IO, NamedTuple
from typing import Callable, IO, NamedTuple, overload

from .kgmvprdataciphers import KGMCryptoLegacy
from ..exceptions import CrypterCreatingError
from ..keyutils import make_salt
from ..prototypes import EncryptedBytesIOSkel
from ..miscutils import proberfuncfactory
from ..prototypes import EncryptedBytesIO
from ..typedefs import BytesLike, FilePath, KeyStreamBasedStreamCipherProto, StreamCipherProto
from ..typeutils import isfilepath, tobytes, verify_fileobj

Expand All @@ -19,15 +20,38 @@

class KGMorVPRFileInfo(NamedTuple):
cipher_data_offset: int
"""加密数据在文件中开始的位置。"""
cipher_data_len: int
"""加密数据在文件中的长度。"""
encryption_version: int
"""加密方法的版本。目前仅支持 3。"""
core_key_slot: int
"""加解密数据所需的密钥槽序号。"""
core_key_test_data: bytes
"""用于验证文件内主密钥合法性的数据。"""
master_key: bytes | None
"""主密钥。需要配合 ``core_key_slot`` 对应的密钥使用。"""
is_vpr: bool
"""如果文件使用了 VPR 加密,则为 ``True``。"""
opener: Callable[[tuple[FilePath | IO[bytes], KGMorVPRFileInfo] | FilePath | IO[bytes], ...], KGMorVPR]
"""打开文件的方式,为一个可调对象,其会返回一个加密文件对象。"""
opener_kwargs_required: tuple[str, ...]
"""通过 ``opener`` 打开文件时,所必需的关键字参数的名称。"""
opener_kwargs_optional: tuple[str, ...]
"""通过 ``opener`` 打开文件时,可选的关键字参数的名称。
此属性仅储存可能会影响 ``opener`` 行为的可选关键字参数;
对 ``opener`` 行为没有影响的可选关键字参数不会出现在此属性中。
"""


@overload
def probe_kgmvpr(filething: FilePath | IO[bytes], /) -> tuple[Path | IO[bytes], KGMorVPRFileInfo | None]:
pass


@proberfuncfactory
def probe_kgmvpr(filething, /):
"""探测源文件 ``filething`` 是否为一个 KGM 或 VPR 文件。
返回一个 2 个元素长度的元组:第一个元素为 ``filething``;如果
Expand All @@ -44,53 +68,43 @@ def probe_kgmvpr(filething: FilePath | IO[bytes], /) -> tuple[Path | IO[bytes],
一个 2 个元素长度的元组:第一个元素为 filething;如果
filething 是 KGM 或 VPR 文件,那么第二个元素为一个 KGMorVPRFileInfo 对象;否则为 None。
"""

def operation(fd: IO[bytes]) -> KGMorVPRFileInfo | None:
total_size = fd.seek(0, 2)
if total_size < 60:
return
fd.seek(0, 0)

header = fd.read(16)
if header == b'\x05\x28\xbc\x96\xe9\xe4\x5a\x43\x91\xaa\xbd\xd0\x7a\xf5\x36\x31':
is_vpr = True
elif header == b'\x7c\xd5\x32\xeb\x86\x02\x7f\x4b\xa8\xaf\xa6\x8e\x0f\xff\x99\x14':
is_vpr = False
else:
return

cipher_data_offset = int.from_bytes(fd.read(4), 'little')
encryption_version = int.from_bytes(fd.read(4), 'little')
core_key_slot = int.from_bytes(fd.read(4), 'little')
core_key_test_data = fd.read(16)
master_key = fd.read(16)

return KGMorVPRFileInfo(
cipher_data_offset=cipher_data_offset,
cipher_data_len=total_size - cipher_data_offset,
encryption_version=encryption_version,
core_key_slot=core_key_slot,
core_key_test_data=core_key_test_data,
master_key=master_key,
is_vpr=is_vpr
)

if isfilepath(filething):
with open(filething, mode='rb') as fileobj:
return Path(filething), operation(fileobj)
opener_kwargs_required = ['table1', 'table2', 'tablev2']

total_size = filething.seek(0, 2)
if total_size < 60:
return
filething.seek(0, 0)

header = filething.read(16)
if header == b'\x05\x28\xbc\x96\xe9\xe4\x5a\x43\x91\xaa\xbd\xd0\x7a\xf5\x36\x31':
is_vpr = True
opener_kwargs_required.append('vpr_key')
elif header == b'\x7c\xd5\x32\xeb\x86\x02\x7f\x4b\xa8\xaf\xa6\x8e\x0f\xff\x99\x14':
is_vpr = False
else:
fileobj = verify_fileobj(filething, 'binary',
verify_readable=True,
verify_seekable=True
)
fileobj_origpos = fileobj.tell()
prs = operation(fileobj)
fileobj.seek(fileobj_origpos, 0)

return fileobj, prs


class KGMorVPR(EncryptedBytesIOSkel):
return

cipher_data_offset = int.from_bytes(filething.read(4), 'little')
encryption_version = int.from_bytes(filething.read(4), 'little')
core_key_slot = int.from_bytes(filething.read(4), 'little')
core_key_test_data = filething.read(16)
master_key = filething.read(16)

return KGMorVPRFileInfo(
cipher_data_offset=cipher_data_offset,
cipher_data_len=total_size - cipher_data_offset,
encryption_version=encryption_version,
core_key_slot=core_key_slot,
core_key_test_data=core_key_test_data,
master_key=master_key,
is_vpr=is_vpr,
opener=KGMorVPR.open,
opener_kwargs_required=tuple(opener_kwargs_required),
opener_kwargs_optional=()
)


class KGMorVPR(EncryptedBytesIO):
"""基于 BytesIO 的 KGM/VPR 透明加密二进制流。
所有读写相关方法都会经过透明加密层处理:
Expand Down Expand Up @@ -201,14 +215,6 @@ def open(cls,
tablev2: 解码表 3
vpr_key: 针对 VPR 文件额外所需的密钥
"""
# if table1 is not None:
# table1 = tobytes(table1)
# if table2 is not None:
# table2 = tobytes(table2)
# if tablev2 is not None:
# tablev2 = tobytes(tablev2)
# if vpr_key is not None:
# vpr_key = tobytes(vpr_key)
table1 = tobytes(table1)
table2 = tobytes(table2)
tablev2 = tobytes(tablev2)
Expand Down
Loading

0 comments on commit 86ce995

Please sign in to comment.