393 lines
15 KiB
Python
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)
|