Skip to main content

Overview

The crypto_utils module provides AES-256-CBC encryption/decryption functionality for handling encrypted provider data and M3U playlist content. It supports multiple decryption keys and automatic key rotation.

Classes

KeyInfo

Dataclass representing AES encryption key information.
@dataclass
class KeyInfo:
    key: bytes
    iv: bytes
key
bytes
required
AES encryption key (32 bytes for AES-256)
iv
bytes
required
Initialization vector for CBC mode (16 bytes)
Example:
from lib.crypto_utils import KeyInfo

key_info = KeyInfo(
    key=bytes.fromhex('0123456789abcdef' * 4),  # 32 bytes
    iv=bytes.fromhex('fedcba9876543210' * 2)    # 16 bytes
)

Functions

decrypt_data()

Decrypts base64-encoded encrypted data using configured AES keys.
def decrypt_data(encrypted_base64: str) -> Optional[str]
encrypted_base64
str
required
Base64-encoded encrypted data (whitespace and line breaks are automatically stripped)
Returns:
decrypted
Optional[str]
Decrypted plaintext string, or None if decryption fails with all available keys
Decryption Process:
  1. Cleans base64 input (removes \n, \r, spaces, tabs)
  2. Decodes from base64 to ciphertext bytes
  3. Attempts decryption with each available key (key1, key2)
  4. Returns the first successful decryption result
  5. Returns None if all keys fail
Validation: Decrypted content is validated by checking if it:
  • Starts with { (JSON object)
  • Starts with [ (JSON array)
  • Contains "http" (URL content)
Example:
from lib.crypto_utils import decrypt_data

encrypted = "SGVsbG8gV29ybGQh..."  # base64-encoded encrypted data
decrypted = decrypt_data(encrypted)

if decrypted:
    print(f"Decrypted: {decrypted}")
else:
    print("Decryption failed")

decrypt_content()

Decrypts M3U playlist content with custom encryption scheme or returns raw content if already decrypted.
def decrypt_content(content: str) -> str
content
str
required
Encrypted or plaintext M3U content
Returns:
content
str
Decrypted M3U content, or original content if decryption fails or content is already plaintext
Behavior:
The function first checks if content is already valid M3U by looking for markers:
  • #EXTM3U
  • #EXTINF
  • #KODIPROP
If found, returns content unchanged. Otherwise, attempts custom decryption.
Custom Decryption Algorithm: For encrypted content (length ≥ 79 characters), the encryption uses a sophisticated string-slicing scheme:
  1. Extract encrypted data parts:
    • Part 1: Characters 0-10
    • Part 2: Characters 34 to -54 (middle section)
    • Part 3: Last 10 characters
    • Combined: part1 + part2 + part3
  2. Extract embedded key and IV:
    • IV (base64): Characters 10-34
    • Key (base64): Characters -54 to -10
  3. Decrypt using AES-256-CBC:
    • Decode key, IV, and encrypted data from base64
    • Use AES.MODE_CBC with embedded key and IV
    • Unpad using PKCS5/PKCS7 padding
Example:
from lib.crypto_utils import decrypt_content

# Encrypted content
encrypted = "dGVzdGRhdGE..."  # Custom encrypted format
decrypted = decrypt_content(encrypted)
print(decrypted)  # #EXTM3U\n#EXTINF:...

# Already decrypted content
plain_m3u = "#EXTM3U\n#EXTINF:-1,Channel\nhttp://..."
result = decrypt_content(plain_m3u)
print(result == plain_m3u)  # True
If decryption fails, the function returns the original content unchanged. Always validate the returned content before parsing.

try_decrypt()

Attempts to decrypt ciphertext using a specific key and IV.
def try_decrypt(ciphertext: bytes, key_info: KeyInfo) -> Optional[str]
ciphertext
bytes
required
Raw encrypted bytes (not base64-encoded)
key_info
KeyInfo
required
KeyInfo object containing the AES key and IV to use
Returns:
plaintext
Optional[str]
Decrypted and validated UTF-8 string, or None if decryption/validation fails
Decryption Steps:
  1. Creates AES cipher in CBC mode with provided key and IV
  2. Decrypts the ciphertext
  3. Removes PKCS5/PKCS7 padding (last byte indicates padding length)
  4. Decodes to UTF-8 string
  5. Validates decrypted content
Validation Criteria: Decrypted text must meet one of these conditions:
  • Starts with { (JSON object)
  • Starts with [ (JSON array)
  • Contains "http" (case-insensitive)
Example:
from lib.crypto_utils import try_decrypt, KeyInfo
import base64

key_info = KeyInfo(
    key=bytes.fromhex('0123456789abcdef' * 4),
    iv=bytes.fromhex('fedcba9876543210' * 2)
)

ciphertext = base64.b64decode("encrypted_data_here")
plaintext = try_decrypt(ciphertext, key_info)

if plaintext:
    print(f"Success: {plaintext}")
else:
    print("Decryption failed or validation failed")

Internal Functions

hex_string_to_bytes()

Converts hexadecimal string to bytes.
def hex_string_to_bytes(hex_str: str) -> bytes
hex_str
str
required
Hexadecimal string (e.g., “0123456789abcdef”)
Returns:
bytes
bytes
Byte representation of the hex string

parse_key_info()

Parses a secret string in the format key_hex:iv_hex into a KeyInfo object.
def parse_key_info(secret: str) -> KeyInfo
secret
str
required
Secret string in format "key_hex:iv_hex" (e.g., "0123...abcd:fedc...3210")
Returns:
key_info
KeyInfo
KeyInfo object with parsed key and IV bytes
Example:
from lib.crypto_utils import parse_key_info

secret = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef:fedcba9876543210fedcba9876543210"
key_info = parse_key_info(secret)
print(len(key_info.key))  # 32 (bytes)
print(len(key_info.iv))   # 16 (bytes)

keys()

Loads and returns all configured decryption keys.
def keys()
Returns:
keys
dict[str, KeyInfo]
Dictionary mapping key names ("key1", "key2") to KeyInfo objects
Key Sources:
Keys are loaded from secret files:
  • SECRET1 from resources/secret1.txt
  • SECRET2 from resources/secret2.txt
Each file contains a hex key and IV in format: key_hex:iv_hex

Configuration

Secret Files

The module reads encryption keys from:
FilePathContent Format
secret1.txt{ADDON_PATH}/resources/secret1.txtkey_hex:iv_hex
secret2.txt{ADDON_PATH}/resources/secret2.txtkey_hex:iv_hex
Example secret file format:
0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef:fedcba9876543210fedcba9876543210
(64 hex chars for key : 32 hex chars for IV)

Encryption Details

Algorithm: AES-256-CBC
Padding: PKCS5/PKCS7
Key Size: 256 bits (32 bytes)
IV Size: 128 bits (16 bytes)
Library: PyCryptodome (Cryptodome.Cipher.AES)
This module requires the pycryptodome package. Ensure it is installed:
pip install pycryptodome

Build docs developers (and LLMs) love