Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

正式版更新:2.1.0 #7

Merged
merged 9 commits into from
Feb 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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