DVDFabExtendedLicense/src/ticket.py
2024-09-30 16:28:53 +02:00

393 lines
15 KiB
Python

import argparse
import hashlib
import json
import re
import secrets
from datetime import datetime, timedelta
from enum import Enum
import requests
from requests_toolbelt import MultipartEncoder
# Constants for default empty values
EMPTY_SERIAL = '00000000000000000000000000000000'
EMPTY_MAC = '00-00-00-00-00-00:00-00-00-00-00-00'
# Enum to represent ticket types
class TicketType(Enum):
SUBSCRIBER = 3
LIFETIME = 2
FREE = 1
NO_LOGIN = 0
# Enum to represent client types (operating systems)
class ClientType(Enum):
WINDOWS = 94
ANDROID = 51
MAC = 0 # TODO: Define the Mac Client ID
class Ticket:
MAGIC = 1
# List of valid product IDs (these could represent different software or features)
PRODUCTS = [
# StreamFab
308, 310, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 337, 339, 340, 342, 346, 348,
349, 350, 352, 353, 356, 357, 358, 359, 360, 361, 362, 364, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375,
376, 377, 378, 379, 380, 381, 500,
# Others
2, 11, 20, 21, 22, 50, 55, 60, 61, 62, 63, 70, 91, 92, 93, 94, 95, 96, 97, 98, 200, 201, 208, 209, 213, 214,
215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 300, 301, 302, 303, 304,
305, 306, 307, 309, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 336, 338, 341, 347, 351, 354, 355, 363,
365, 384, 394, 396, 397, 398, 400, 401, 402, 403, 404, 405, 407, 409, 410, 412, 414, 1002, 1011, 1020, 1021,
1022, 1050, 1055, 1060, 1061, 1062, 1070, 1095, 1096, 1097, 1098, 1200, 1201, 1208, 1209, 1213, 1214, 1215,
1216, 1217, 1218, 1219, 1221, 1222, 1223, 1224, 1225, 1226, 1227, 1228, 1229, 1230, 1231, 1300, 1301, 1302,
1303, 1304, 1305, 1306, 1307, 1308, 1310, 1312, 1313, 1314, 1315, 1316, 1317, 1318, 1319, 1320, 1322, 1323,
1324, 1325, 1326, 1327, 1328, 1329, 1330, 1331, 1332, 1333, 1334, 1335, 1336, 1337, 1338, 1339, 1340, 1341,
1342, 1346, 1347, 1348, 1349, 1350, 1351, 1352, 1353, 1354, 1355, 1356, 1357, 1358, 1359, 1360, 1361, 1362,
1363, 1364, 1365, 1366, 1367, 1369, 1370, 1371, 1372, 1373, 1374, 1375, 1376, 1377, 1378]
def __init__(self, token: str, data: dict):
"""
Initialize the ticket with a user token and relevant ticket data.
Args:
token (str): User's unique token.
data (dict): Dictionary containing ticket information (type, version, expiration).
"""
self.token = token
self.type = TicketType[data['type']]
self.version = data['version']
self.expire = data['expire']
def __repr__(self) -> str:
"""Provide string representation for debugging."""
return '{name}({items})'.format(
name=self.__class__.__name__,
items=', '.join([f'{k}={repr(v)}' for k, v in self.__dict__.items()])
)
def export(self) -> str:
"""
Export the ticket information into a serialized format.
This method constructs a string representing the ticket data and expiration date.
Returns:
str: A serialized string representation of the ticket, including magic number,
product details, and configuration.
"""
current = datetime.now() # Current date and time
# Expiration date
if self.type == TicketType.LIFETIME:
expire = datetime.fromisoformat('2231-10-10T13:22:44')
else:
expire = current + timedelta(days=self.expire)
# Ticket configuration based on ticket type
config = {
'VP': -1, # Validity period (-1 for lifetime/subscription)
'VPT': 0, # Validity period time (specific expiration timestamp for non-lifetime tickets)
'OV': self.version, # Software version
'BV': '', # Base version (reserved for future use)
'AD': 1, # Additional field (1 for Subscriber and Lifetime)
'SUB': '', # Subscription identifier (if available)
'UT': 0, # Usage Type
'UL': 1, # User Login required flag
'ML': (0, 6, 1), # Machine limits or other codes (used machines, max allowed, currently active)
'S': self.token, # User's token
'TI': round(current.timestamp()), # Ticket issued timestamp
'TM': 0 # Reserved field
}
# Adjust config for different ticket types
if self.type == TicketType.NO_LOGIN:
# For NO_LOGIN type tickets, certain fields are removed
del config['AD']
del config['S']
else:
del config['UL'] # No need for user login in other types
config['ML'] = (1, 6, 2) # Adjust machine limits
if self.type == TicketType.SUBSCRIBER:
config['VP'] = (expire - current).days # Set validity period for Subscriber type
config['VPT'] = round(expire.timestamp()) # Set the expiration timestamp
if self.type == TicketType.FREE:
config['ML'] = (1, 1, 1) # Restrict machine limits for Free ticket type
# Convert machine limits to string format
config['ML'] = '-'.join(map(str, config['ML']))
# Construct the ticket export string with MAGIC number and product details
items = [self.MAGIC]
# Determine products that this ticket covers
if self.type in (TicketType.LIFETIME, TicketType.SUBSCRIBER):
# Add products with expiration timestamps
items += [f'{p}:{round(expire.timestamp())}' for p in self.PRODUCTS]
# Add configuration key-value pairs
items += [f'{k}:{v}' for k, v in config.items()]
# Return the serialized string
return '|'.join(map(str, items))
@classmethod
def from_token(cls, data: dict):
"""
Create a ticket instance using an existing user token.
This method validates the token length and initializes a new Ticket instance
using the provided token and data.
Args:
data (dict): Dictionary containing token and ticket data.
Returns:
Ticket: An instance of Ticket created using the provided token.
"""
token = data['token']
# Token must be 32 characters long
assert len(token) == 32, 'Invalid Token Length'
# Return a new Ticket instance
return cls(token, data)
@classmethod
def from_account(cls, data: dict):
"""
Create a ticket instance using account credentials.
This method sends a login request to the server and retrieves the token and ticket details.
Args:
data (dict): Dictionary containing account login credentials (email and password).
Returns:
Ticket: An instance of Ticket initialized with the retrieved token and ticket data.
"""
mp_encoder = MultipartEncoder({
'DI': json.dumps({
'MAC': data['mac'],
'DS': data['DS'],
'IS': data['IS'],
'BI': data['BI']
}, separators=(',', ':')),
'H': data['mac'],
'V': str(data['version']),
'C': str(ClientType[data['client']].value),
'S': '',
'U': data['email'],
'P': hashlib.md5(data['password'].encode('utf-8')).hexdigest(),
'T': '0'
})
r = requests.request(
method='POST',
url='https://www.dvdfabstore.com/auth/v5/',
data=mp_encoder,
headers={
'Accept': '*/*',
'Content-Type': mp_encoder.content_type,
'User-Agent': 'FabApp/3.0'
}
)
r.raise_for_status()
# Extract token from response
m = re.search(r'S:([a-f0-9]+)', r.text)
assert m, 'Invalid Login Credentials'
# Return a new Ticket instance
return cls(m.group(1), data)
@staticmethod
def extract(value: str) -> dict:
"""Extract ticket details and return relevant information.
Args:
value (str): The ticket string to extract details from.
Returns:
dict: A dictionary containing the type of ticket, expiration time,
number of products, and configuration details.
"""
items = value.split('|')
current = round(datetime.now().timestamp())
assert int(items[0]) == Ticket.MAGIC, 'Invalid Magic Ticket'
config = {}
expires = []
products = []
for item in items[1:]:
key, value = item.split(':')
# Attempt to convert the value to an integer
try:
value = int(value)
except ValueError:
pass
if key.isdigit():
# Save expiration timestamp
expires.append(value)
# Verify product ID
key = int(key)
if key not in Ticket.PRODUCTS:
print(f'[!] Unknown product: {key}')
# Add the valid product ID to the list
products.append(key)
# Check if the product has expired
if value < current:
print(f'[!] Expired product: {key}')
else:
if key == 'ML':
# Extract used device
value = tuple(map(int, value.split('-')))
# Add non-product key-value pairs to config
config[key] = value
# Determine the type of ticket based on the extracted configuration
if 'UL' in config:
_type = TicketType.NO_LOGIN
elif config['VP'] != -1:
_type = TicketType.SUBSCRIBER
elif config['ML'] == (1, 1, 1):
_type = TicketType.FREE
else:
_type = TicketType.LIFETIME
# Return a dictionary containing the type, expiration, number of products, and configuration
return {
'type': _type.name,
'expire': timedelta(seconds=max(expires) - current).days if expires else 0,
'token': config.get('S'),
'products': len(products),
'devices': config['ML'],
'version': config['OV']
}
@classmethod
def from_ticket(cls, data: dict):
"""Create a ticket from a previous ticket.
Args:
data (dict): Dictionary containing ticket data.
Returns:
Ticket: An instance of Ticket created from the given data.
"""
# Extract details from the ticket
extracted = cls.extract(data['ticket'])
# Retrieve user token from extracted data
token = extracted['token']
assert token, 'Missing User Token'
# Update the software version in the provided data
data['version'] = extracted['version']
# Return a new Ticket instance
return cls(token, data)
@classmethod
def create(cls, data: dict):
"""Generate a new ticket with default values.
Args:
data (dict): Dictionary containing ticket data.
Returns:
Ticket: An instance of Ticket with a newly generated token.
"""
# Generate a secure random token
token = secrets.token_hex(16)
# Return a new Ticket instance with the generated token
return cls(token, data)
if __name__ == '__main__':
# Set up argument parsing
parser = argparse.ArgumentParser(description='DVDFab-Ticket: Manage user tickets for DVDFab software.')
# Subparsers for account actions
subparsers = parser.add_subparsers(dest='action', help='Ticket management actions')
# Subparser for login
login_parser = subparsers.add_parser('login', help='Use account credentials')
login_parser.add_argument('email', type=str, metavar='email', help='Email address')
login_parser.add_argument('password', type=str, metavar='password', help='Password')
login_parser.add_argument('--client', required=False, type=str, choices=[t.name for t in list(ClientType)], default=ClientType.WINDOWS.name, help='Client type')
login_parser.add_argument('--mac', required=False, type=str, metavar='<mac>', default=EMPTY_MAC, help='MAC address')
login_parser.add_argument('--DS', required=False, type=str, metavar='<DS>', default=EMPTY_SERIAL, help='Device serial')
login_parser.add_argument('--IS', required=False, type=str, metavar='<IS>', default=EMPTY_SERIAL, help='Device ID')
login_parser.add_argument('--BI', required=False, type=str, metavar='<BI>', default=EMPTY_SERIAL, help='BIOS information')
# Subparser for token
# Allows user to log in using an existing token rather than credentials
token_parser = subparsers.add_parser('token', help='Use an existing user token')
token_parser.add_argument('token', type=str, metavar='token', help='User token')
# Subparser for ticket
# Allows re-use of a previous ticket
ticket_parser = subparsers.add_parser('ticket', help='Use an existing ticket')
ticket_parser.add_argument('ticket', type=str, metavar='ticket', help='Old user ticket')
# Subparser for extracting info
# Allows users to extract and view information from an existing ticket string
info_parser = subparsers.add_parser('info', help='Extract info from ticket')
info_parser.add_argument('ticket', type=str, metavar='ticket', help='Ticket string')
# Arguments for creating a new ticket
# Allow users to customize the ticket type, version, and expiration
parser.add_argument('--type', required=False, type=str, choices=[t.name for t in list(TicketType)], default=TicketType.SUBSCRIBER.name, help='Ticket type')
parser.add_argument('--version', required=False, type=int, metavar='<version>', default=6200, help='Software version')
parser.add_argument('--expire', required=False, type=int, metavar='<expire>', default=365, help='Days until ticket expires (default: 365)')
# Parse the command-line arguments
args = parser.parse_args()
try:
# Handle ticket information extraction
if args.action == 'info':
infos = Ticket.extract(args.ticket)
for key, value in infos.items():
print(f'[*] {str(key).capitalize()}: {value}')
else:
# Handle ticket creation or loading based on token, login, or ticket
if args.action == 'token':
# Generate ticket from an existing token
ticket = Ticket.from_token(vars(args))
elif args.action == 'login':
# Generate ticket after logging in
ticket = Ticket.from_account(vars(args))
elif args.action == 'ticket':
# Generate ticket from a previous ticket
ticket = Ticket.from_ticket(vars(args))
else:
# Create a new ticket with default settings
ticket = Ticket.create(vars(args))
# Output the serialized ticket string
print(ticket.export())
except Exception as e:
# Print any errors that occur during execution
print(f'[!] {e}')
exit(1)