Files
tvboxzt/py/base/secspider.py
T
2026-04-25 17:11:26 +08:00

126 lines
4.0 KiB
Python

from __future__ import annotations
import base64
import secrets
from pathlib import Path
from Crypto.Cipher import AES
from Crypto.Hash import SHA256
from Crypto.Protocol.KDF import HKDF
from Crypto.PublicKey import ECC
from Crypto.Signature import eddsa
def _derive_wrap_material(master_secret: bytes, kid: str, name: str, version: str) -> tuple[bytes, bytes]:
wrap_key = HKDF(
master=master_secret,
key_len=32,
salt=kid.encode("utf-8"),
hashmod=SHA256,
num_keys=1,
context=f"secspider:{name}:{version}:wrap-key".encode("utf-8"),
)
wrap_nonce = HKDF(
master=master_secret,
key_len=12,
salt=kid.encode("utf-8"),
hashmod=SHA256,
num_keys=1,
context=f"secspider:{name}:{version}:wrap-nonce".encode("utf-8"),
)
return wrap_key, wrap_nonce
def _signing_bytes(headers: dict[str, str], payload_b64: str) -> bytes:
return "\n".join(
[
f"//@name:{headers['name']}",
f"//@version:{headers['version']}",
f"//@remark:{headers['remark']}",
f"//@format:{headers['format']}",
f"//@alg:{headers['alg']}",
f"//@wrap:{headers['wrap']}",
f"//@sign:{headers['sign']}",
f"//@kid:{headers['kid']}",
f"//@nonce:{headers['nonce']}",
f"//@ek:{headers['ek']}",
f"//@hash:{headers['hash']}",
f"payload.base64:{payload_b64}",
]
).encode("utf-8")
def build_secspider_package(
*,
source_text: str,
name: str,
version: str,
remark: str,
kid: str,
signing_private_key,
master_secret: bytes,
) -> str:
source_bytes = source_text.encode("utf-8")
source_hash = SHA256.new(source_bytes).hexdigest()
content_key = secrets.token_bytes(32)
payload_nonce = secrets.token_bytes(12)
wrap_key, wrap_nonce = _derive_wrap_material(master_secret, kid, name, version)
wrap_cipher = AES.new(wrap_key, AES.MODE_GCM, nonce=wrap_nonce)
wrapped_key, wrapped_tag = wrap_cipher.encrypt_and_digest(content_key)
payload_cipher = AES.new(content_key, AES.MODE_GCM, nonce=payload_nonce)
payload_ciphertext, payload_tag = payload_cipher.encrypt_and_digest(source_bytes)
payload_b64 = base64.b64encode(payload_ciphertext + payload_tag).decode("ascii")
headers = {
"name": name,
"version": version,
"remark": remark,
"format": "secspider/1",
"alg": "aes-256-gcm",
"wrap": "hkdf-aes-keywrap",
"sign": "ed25519",
"kid": kid,
"nonce": "base64:" + base64.b64encode(payload_nonce).decode("ascii"),
"ek": "base64:" + base64.b64encode(wrapped_key + wrapped_tag).decode("ascii"),
"hash": f"sha256:{source_hash}",
}
signer = eddsa.new(signing_private_key, "rfc8032")
headers["sig"] = "base64:" + base64.b64encode(signer.sign(_signing_bytes(headers, payload_b64))).decode("ascii")
return "\n".join(
[
"// ignore",
f"//@name:{headers['name']}",
f"//@version:{headers['version']}",
f"//@remark:{headers['remark']}",
f"//@format:{headers['format']}",
f"//@alg:{headers['alg']}",
f"//@wrap:{headers['wrap']}",
f"//@sign:{headers['sign']}",
f"//@kid:{headers['kid']}",
f"//@nonce:{headers['nonce']}",
f"//@ek:{headers['ek']}",
f"//@hash:{headers['hash']}",
f"//@sig:{headers['sig']}",
"// ignore",
f"payload.base64:{payload_b64}",
]
)
def generate_signing_keypair() -> tuple[str, str]:
private_key = ECC.generate(curve="Ed25519")
public_key = private_key.public_key()
return private_key.export_key(format="PEM"), public_key.export_key(format="PEM")
def load_signing_private_key(path: str | Path):
return ECC.import_key(Path(path).read_text(encoding="utf-8"))
def load_master_secret(path: str | Path) -> bytes:
return Path(path).read_text(encoding="utf-8").strip().encode("utf-8")