Last active
January 26, 2026 09:27
-
-
Save mainframed/d1ad035b7da1a7264cea1c9727baebd3 to your computer and use it in GitHub Desktop.
ltpa_generate.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| LTPA Token Generator - Generates IBM WebSphere LTPA v1 and LTPA2 tokens | |
| Based on analysis of IBM LTPA token format and cryptographic implementation. | |
| References: | |
| - http://tech.srij.it/2012/04/how-to-decrypt-ltpa-cookie-and-view.html | |
| - IBM WebSphere LTPA documentation | |
| - WebSphere {xor} password decoder by Jeroen Zomer | |
| Usage: | |
| python ltpa_token_generator.py -k ltpa.keys -p <password> -u <username> [-v] [--version 1|2] | |
| """ | |
| import argparse | |
| import base64 | |
| import hashlib | |
| import sys | |
| from datetime import datetime, timedelta | |
| from pathlib import Path | |
| from typing import Optional | |
| def decode_websphere_password(encoded_password: str, verbose: bool = False) -> str: | |
| """ | |
| Decode a WebSphere {xor} encoded password. | |
| WebSphere uses a simple XOR encoding with underscore (ASCII 95) followed by Base64. | |
| Based on: WebSphere password decoder by Jeroen Zomer (jzomer@strelitzia.net) | |
| Args: | |
| encoded_password: Password string, optionally prefixed with {xor} | |
| verbose: Print debug information | |
| Returns: | |
| Decoded plaintext password | |
| """ | |
| password = encoded_password | |
| # Strip {xor} prefix if present (case-insensitive) | |
| if password.upper().startswith('{XOR}'): | |
| password = password[5:] | |
| if verbose: | |
| print(f"[DEBUG] Detected {{xor}} encoded password") | |
| else: | |
| # Not encoded, return as-is | |
| return encoded_password | |
| # Base64 decode | |
| try: | |
| decoded_bytes = base64.b64decode(password) | |
| except Exception as e: | |
| raise ValueError(f"Failed to Base64 decode password: {e}") | |
| # XOR each byte with underscore (ASCII 95) | |
| plaintext = ''.join(chr(b ^ 95) for b in decoded_bytes) | |
| if verbose: | |
| print(f"[DEBUG] Decoded {{xor}} password: {'*' * len(plaintext)} ({len(plaintext)} chars)") | |
| return plaintext | |
| def encode_websphere_password(plaintext: str) -> str: | |
| """ | |
| Encode a password using WebSphere {xor} encoding. | |
| Args: | |
| plaintext: Plain text password | |
| Returns: | |
| Encoded password with {xor} prefix | |
| """ | |
| # XOR each character with underscore (ASCII 95) | |
| xored = bytes(ord(c) ^ 95 for c in plaintext) | |
| # Base64 encode | |
| encoded = base64.b64encode(xored).decode('utf-8') | |
| return '{xor}' + encoded | |
| # Cryptography imports | |
| from Crypto.Cipher import DES3, AES | |
| from Crypto.PublicKey import RSA | |
| from Crypto.Signature import pkcs1_15 | |
| from Crypto.Hash import SHA1 | |
| from Crypto.Util.Padding import pad, unpad | |
| class LTPAKeysFile: | |
| """Parser for IBM LTPA keys file""" | |
| def __init__(self, filepath: str, password: str, verbose: bool = False): | |
| self.filepath = filepath | |
| self.verbose = verbose | |
| # Decode password if it's {xor} encoded | |
| self.password = decode_websphere_password(password, verbose=verbose) | |
| # Parsed values from keys file | |
| self.version: str = "" | |
| self.realm: str = "" | |
| self.encrypted_3des_key: bytes = b"" | |
| self.encrypted_private_key: bytes = b"" | |
| self.public_key_bytes: bytes = b"" | |
| self.creation_date: str = "" | |
| self.creation_host: str = "" | |
| # Decrypted keys | |
| self.des3_key: bytes = b"" | |
| self.aes_key: bytes = b"" | |
| self.private_key: Optional[RSA.RsaKey] = None | |
| self.public_key: Optional[RSA.RsaKey] = None | |
| self._parse_keys_file() | |
| self._decrypt_keys() | |
| def _log(self, message: str): | |
| """Print verbose log message""" | |
| if self.verbose: | |
| print(f"[DEBUG] {message}") | |
| def _parse_keys_file(self): | |
| """Parse the ltpa.keys file""" | |
| self._log(f"Parsing keys file: {self.filepath}") | |
| with open(self.filepath, 'r') as f: | |
| for line in f: | |
| line = line.strip() | |
| if line.startswith('#') or '=' not in line: | |
| continue | |
| # Handle escaped colons in values | |
| key, value = line.split('=', 1) | |
| value = value.replace('\\:', ':') | |
| if key == "com.ibm.websphere.ltpa.version": | |
| self.version = value | |
| self._log(f"LTPA Version: {value}") | |
| elif key == "com.ibm.websphere.ltpa.Realm": | |
| self.realm = value | |
| self._log(f"Realm: {value}") | |
| elif key == "com.ibm.websphere.ltpa.3DESKey": | |
| self.encrypted_3des_key = base64.b64decode(value) | |
| self._log(f"3DES Key (encrypted): {len(self.encrypted_3des_key)} bytes") | |
| elif key == "com.ibm.websphere.ltpa.PrivateKey": | |
| self.encrypted_private_key = base64.b64decode(value) | |
| self._log(f"Private Key (encrypted): {len(self.encrypted_private_key)} bytes") | |
| elif key == "com.ibm.websphere.ltpa.PublicKey": | |
| self.public_key_bytes = base64.b64decode(value) | |
| self._log(f"Public Key: {len(self.public_key_bytes)} bytes") | |
| elif key == "com.ibm.websphere.CreationDate": | |
| self.creation_date = value | |
| self._log(f"Creation Date: {value}") | |
| elif key == "com.ibm.websphere.CreationHost": | |
| self.creation_host = value | |
| self._log(f"Creation Host: {value}") | |
| def _derive_password_key(self) -> bytes: | |
| """ | |
| Derive 3DES key from password using SHA-1 | |
| The password is hashed with SHA-1 (20 bytes), then padded to 24 bytes for 3DES | |
| """ | |
| sha1_hash = hashlib.sha1(self.password.encode('utf-8')).digest() | |
| # Pad to 24 bytes (3DES key size) with zeros | |
| key_3des = sha1_hash + b'\x00' * 4 | |
| self._log(f"Password-derived 3DES key: {len(key_3des)} bytes") | |
| return key_3des | |
| def _decrypt_keys(self): | |
| """Decrypt the 3DES key and private key using the password""" | |
| password_key = self._derive_password_key() | |
| # Decrypt the 3DES key using password-derived key (ECB mode) | |
| cipher = DES3.new(password_key, DES3.MODE_ECB) | |
| self.des3_key = unpad(cipher.decrypt(self.encrypted_3des_key), DES3.block_size) | |
| self._log(f"Decrypted 3DES key: {len(self.des3_key)} bytes") | |
| # Derive AES key from 3DES key for LTPA2 | |
| # AES key is first 16 bytes of SHA-1 hash of the 3DES key | |
| self.aes_key = hashlib.sha1(self.des3_key).digest()[:16] | |
| self._log(f"Derived AES key: {len(self.aes_key)} bytes") | |
| # Decrypt and parse the private key first (it contains all we need) | |
| self._decrypt_private_key(password_key) | |
| # Parse public key (optional, for verification) | |
| try: | |
| self._parse_public_key() | |
| except Exception as e: | |
| self._log(f"Note: Could not parse public key separately: {e}") | |
| # That's OK - we already have n and e from the private key | |
| def _decrypt_private_key(self, password_key: bytes): | |
| """Decrypt and parse the RSA private key""" | |
| cipher = DES3.new(password_key, DES3.MODE_ECB) | |
| decrypted = unpad(cipher.decrypt(self.encrypted_private_key), DES3.block_size) | |
| self._log(f"Decrypted private key data: {len(decrypted)} bytes") | |
| # Parse the IBM private key format | |
| # Format: shared_key_length(4) + shared_key + private_exponent_length(4) + private_exponent | |
| try: | |
| self.private_key = self._parse_ibm_private_key(decrypted) | |
| self._log(f"RSA Private key parsed successfully, size: {self.private_key.size_in_bits()} bits") | |
| except Exception as e: | |
| self._log(f"Warning: Could not parse private key: {e}") | |
| def _parse_ibm_private_key(self, data: bytes) -> RSA.RsaKey: | |
| """ | |
| Parse IBM's custom private key format. | |
| Based on oniyi-ltpa (https://github.com/benkroeger/oniyi-ltpa): | |
| The private key buffer structure is: | |
| - 4 bytes: left-pad (unused) | |
| - variable: private exponent (d) | |
| - 3 bytes: public exponent (e) | |
| - 65 bytes: prime1 (p) | |
| - 65 bytes: prime2 (q) | |
| The private exponent length = total_length - 4 - 3 - 65 - 65 | |
| """ | |
| self._log(f"Private key raw data: {len(data)} bytes") | |
| self._log(f"Private key hex (first 20): {data[:20].hex()}") | |
| self._log(f"Private key hex (last 20): {data[-20:].hex()}") | |
| total_len = len(data) | |
| # Fixed sizes at the end | |
| PRIME_SIZE = 65 # bytes for p and q | |
| EXPONENT_SIZE = 3 # bytes for public exponent e | |
| LEFT_PAD = 4 # unused bytes at start | |
| # Calculate private exponent length | |
| d_len = total_len - LEFT_PAD - EXPONENT_SIZE - (2 * PRIME_SIZE) | |
| self._log(f"Calculated private exponent length: {d_len} bytes") | |
| if d_len <= 0: | |
| raise ValueError(f"Invalid private key structure: calculated d_len={d_len}") | |
| offset = LEFT_PAD | |
| # Read private exponent (d) | |
| d = int.from_bytes(data[offset:offset + d_len], 'big') | |
| offset += d_len | |
| self._log(f"Private exponent (d): {d.bit_length()} bits") | |
| # Read public exponent (e) - 3 bytes | |
| e = int.from_bytes(data[offset:offset + EXPONENT_SIZE], 'big') | |
| offset += EXPONENT_SIZE | |
| self._log(f"Public exponent (e): {e}") | |
| # Read prime1 (p) - 65 bytes | |
| p = int.from_bytes(data[offset:offset + PRIME_SIZE], 'big') | |
| offset += PRIME_SIZE | |
| self._log(f"Prime p: {p.bit_length()} bits") | |
| # Read prime2 (q) - 65 bytes | |
| q = int.from_bytes(data[offset:offset + PRIME_SIZE], 'big') | |
| self._log(f"Prime q: {q.bit_length()} bits") | |
| # Calculate modulus n = p * q | |
| n = p * q | |
| self._log(f"Calculated modulus n: {n.bit_length()} bits") | |
| # Verify: d * e ≡ 1 (mod φ(n)) where φ(n) = (p-1)(q-1) | |
| phi_n = (p - 1) * (q - 1) | |
| check = (d * e) % phi_n | |
| self._log(f"Verification d*e mod φ(n) = {check} (should be 1)") | |
| # Construct the RSA private key with all components | |
| # RSA.construct expects: (n, e, d) or (n, e, d, p, q) | |
| return RSA.construct((n, e, d, p, q)) | |
| def _parse_public_key(self): | |
| """ | |
| Parse IBM's public key format. | |
| The public key format appears to be similar to the private key: | |
| - Some header bytes | |
| - 3 bytes: public exponent (e) | |
| - remaining: modulus (n) | |
| However, since we already have n and e from parsing the private key, | |
| this is mainly for verification. | |
| """ | |
| data = self.public_key_bytes | |
| self._log(f"Public key raw data: {len(data)} bytes") | |
| self._log(f"Public key hex dump (first 20 bytes): {data[:20].hex()}") | |
| self._log(f"Public key hex dump (last 10 bytes): {data[-10:].hex()}") | |
| # If we already have a private key parsed, we can use its components | |
| if self.private_key is not None: | |
| self._log("Using n and e from already-parsed private key") | |
| self.public_key = RSA.construct((self.private_key.n, self.private_key.e)) | |
| self._log(f"RSA Public key constructed, size: {self.public_key.size_in_bits()} bits") | |
| return | |
| # Otherwise try to parse the public key directly | |
| # The format seems to be: 4-byte header + 3-byte exponent + modulus | |
| # But let's be flexible | |
| total_len = len(data) | |
| # Try format: skip first few bytes, last 3 bytes might be exponent or first 3 after header | |
| # Based on private key format, let's try: header(4) + e(3) + n(remaining) | |
| HEADER_SIZE = 4 | |
| EXPONENT_SIZE = 3 | |
| if total_len > HEADER_SIZE + EXPONENT_SIZE: | |
| offset = HEADER_SIZE | |
| # Read exponent (e) - 3 bytes | |
| e = int.from_bytes(data[offset:offset + EXPONENT_SIZE], 'big') | |
| offset += EXPONENT_SIZE | |
| self._log(f"Public key exponent (e) attempt: {e}") | |
| # Check if e is reasonable (common values: 3, 17, 65537) | |
| if e in [3, 17, 65537] or (e > 2 and e % 2 == 1): | |
| # Read modulus | |
| n = int.from_bytes(data[offset:], 'big') | |
| self._log(f"Public key modulus (n): {n.bit_length()} bits") | |
| self.public_key = RSA.construct((n, e)) | |
| self._log(f"RSA Public key parsed, size: {self.public_key.size_in_bits()} bits") | |
| else: | |
| raise ValueError(f"Unexpected public exponent value: {e}") | |
| class LTPATokenGenerator: | |
| """Generate LTPA v1 and LTPA2 tokens""" | |
| def __init__(self, keys: LTPAKeysFile, verbose: bool = False): | |
| self.keys = keys | |
| self.verbose = verbose | |
| def _log(self, message: str): | |
| """Print verbose log message""" | |
| if self.verbose: | |
| print(f"[DEBUG] {message}") | |
| def generate_ltpa1_token(self, username: str, expiration_time: Optional[datetime] = None, | |
| token_lifetime_minutes: int = 120) -> str: | |
| """ | |
| Generate an LTPA v1 token | |
| Format: userInfo%expiration%signature | |
| - userInfo: u:realm/username | |
| - expiration: Unix timestamp in milliseconds | |
| - signature: Base64-encoded signature | |
| The entire token is then encrypted with 3DES and Base64 encoded | |
| """ | |
| # Calculate expiration | |
| if expiration_time is None: | |
| expiration_time = datetime.now() + timedelta(minutes=token_lifetime_minutes) | |
| expiration_ms = int(expiration_time.timestamp() * 1000) | |
| # Build user info string | |
| user_info = f"u:{self.keys.realm}/{username}" | |
| self._log(f"User info: {user_info}") | |
| self._log(f"Expiration: {expiration_time} ({expiration_ms})") | |
| # Create the token body (without signature) | |
| token_body = f"{user_info}%{expiration_ms}" | |
| # Create signature | |
| signature = self._sign_token(token_body.encode('utf-8')) | |
| signature_b64 = base64.b64encode(signature).decode('utf-8') | |
| self._log(f"Signature (base64): {signature_b64[:50]}...") | |
| # Complete token | |
| full_token = f"{token_body}%{signature_b64}" | |
| self._log(f"Full token (plaintext): {full_token[:80]}...") | |
| # Encrypt with 3DES (ECB mode for LTPA v1) | |
| # Pad the 3DES key to 24 bytes if needed | |
| des3_key = self.keys.des3_key | |
| if len(des3_key) < 24: | |
| des3_key = des3_key + b'\x00' * (24 - len(des3_key)) | |
| cipher = DES3.new(des3_key[:24], DES3.MODE_ECB) | |
| encrypted = cipher.encrypt(pad(full_token.encode('utf-8'), DES3.block_size)) | |
| # Base64 encode | |
| token = base64.b64encode(encrypted).decode('utf-8') | |
| self._log(f"Final LTPA v1 token length: {len(token)}") | |
| return token | |
| def generate_ltpa2_token(self, username: str, expiration_time: Optional[datetime] = None, | |
| token_lifetime_minutes: int = 120, | |
| attributes: Optional[dict] = None) -> str: | |
| """ | |
| Generate an LTPA2 token | |
| LTPA2 format is more complex with attribute pairs: | |
| expire:timestamp$u:user:realm/username$attributes%signature | |
| Encrypted with AES-128-CBC | |
| """ | |
| # Calculate expiration | |
| if expiration_time is None: | |
| expiration_time = datetime.now() + timedelta(minutes=token_lifetime_minutes) | |
| expiration_ms = int(expiration_time.timestamp() * 1000) | |
| # Build LTPA2 token body | |
| # Format: expire:<timestamp>$u:user:<realm>/<username> | |
| user_data = f"u:user:{self.keys.realm}/{username}" | |
| token_body = f"expire:{expiration_ms}${user_data}" | |
| # Add any additional attributes | |
| if attributes: | |
| for key, value in attributes.items(): | |
| token_body += f"${key}:{value}" | |
| self._log(f"LTPA2 token body: {token_body}") | |
| # Create signature over the token body | |
| signature = self._sign_token(token_body.encode('utf-8')) | |
| signature_b64 = base64.b64encode(signature).decode('utf-8') | |
| self._log(f"LTPA2 Signature (base64): {signature_b64[:50]}...") | |
| # Complete token with signature | |
| full_token = f"{token_body}%{signature_b64}" | |
| self._log(f"LTPA2 full token (plaintext): {full_token[:100]}...") | |
| # Encrypt with AES-128-CBC | |
| # IV is the first 16 bytes of the AES key | |
| iv = self.keys.aes_key[:16] | |
| cipher = AES.new(self.keys.aes_key, AES.MODE_CBC, iv) | |
| encrypted = cipher.encrypt(pad(full_token.encode('utf-8'), AES.block_size)) | |
| # Base64 encode | |
| token = base64.b64encode(encrypted).decode('utf-8') | |
| self._log(f"Final LTPA2 token length: {len(token)}") | |
| return token | |
| def _sign_token(self, data: bytes) -> bytes: | |
| """Sign token data with RSA private key using SHA-1""" | |
| if self.keys.private_key is None: | |
| raise ValueError("Private key not available for signing") | |
| h = SHA1.new(data) | |
| signature = pkcs1_15.new(self.keys.private_key).sign(h) | |
| return signature | |
| def decode_ltpa1_token(self, token: str) -> dict: | |
| """Decode and decrypt an LTPA v1 token (useful for verification)""" | |
| # Base64 decode | |
| encrypted = base64.b64decode(token) | |
| # Decrypt with 3DES | |
| des3_key = self.keys.des3_key | |
| if len(des3_key) < 24: | |
| des3_key = des3_key + b'\x00' * (24 - len(des3_key)) | |
| cipher = DES3.new(des3_key[:24], DES3.MODE_ECB) | |
| decrypted = unpad(cipher.decrypt(encrypted), DES3.block_size) | |
| plaintext = decrypted.decode('utf-8') | |
| # Parse token | |
| parts = plaintext.split('%') | |
| return { | |
| 'user_info': parts[0] if len(parts) > 0 else '', | |
| 'expiration': int(parts[1]) if len(parts) > 1 else 0, | |
| 'expiration_date': datetime.fromtimestamp(int(parts[1]) / 1000) if len(parts) > 1 else None, | |
| 'signature': parts[2] if len(parts) > 2 else '', | |
| 'raw': plaintext | |
| } | |
| def decode_ltpa2_token(self, token: str) -> dict: | |
| """Decode and decrypt an LTPA2 token (useful for verification)""" | |
| # Base64 decode | |
| encrypted = base64.b64decode(token) | |
| # Decrypt with AES-128-CBC | |
| iv = self.keys.aes_key[:16] | |
| cipher = AES.new(self.keys.aes_key, AES.MODE_CBC, iv) | |
| decrypted = unpad(cipher.decrypt(encrypted), AES.block_size) | |
| plaintext = decrypted.decode('utf-8') | |
| # Parse token - split on % to separate body from signature | |
| parts = plaintext.split('%') | |
| body = parts[0] if len(parts) > 0 else '' | |
| signature = parts[1] if len(parts) > 1 else '' | |
| # Parse attributes from body | |
| attributes = {} | |
| for attr in body.split('$'): | |
| if ':' in attr: | |
| key, value = attr.split(':', 1) | |
| attributes[key] = value | |
| expiration_ms = int(attributes.get('expire', 0)) | |
| return { | |
| 'attributes': attributes, | |
| 'expiration': expiration_ms, | |
| 'expiration_date': datetime.fromtimestamp(expiration_ms / 1000) if expiration_ms else None, | |
| 'signature': signature, | |
| 'raw': plaintext | |
| } | |
| def parse_datetime(dt_string: str) -> datetime: | |
| """Parse a datetime string in various formats""" | |
| formats = [ | |
| '%Y-%m-%d %H:%M:%S', | |
| '%Y-%m-%dT%H:%M:%S', | |
| '%Y-%m-%d %H:%M', | |
| '%Y-%m-%d', | |
| ] | |
| for fmt in formats: | |
| try: | |
| return datetime.strptime(dt_string, fmt) | |
| except ValueError: | |
| continue | |
| raise ValueError(f"Could not parse datetime: {dt_string}. " | |
| f"Use format: YYYY-MM-DD HH:MM:SS") | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description='Generate IBM WebSphere LTPA v1 and LTPA2 tokens', | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| epilog=""" | |
| Examples: | |
| # Generate LTPA v1 token | |
| %(prog)s -k ltpa.keys -p mypassword -u jsmith | |
| # Generate LTPA2 token | |
| %(prog)s -k ltpa.keys -p mypassword -u jsmith --version 2 | |
| # Generate both tokens with custom expiration | |
| %(prog)s -k ltpa.keys -p mypassword -u jsmith --version both --expiry "2024-12-31 23:59:59" | |
| # Generate with verbose output and custom lifetime | |
| %(prog)s -k ltpa.keys -p mypassword -u jsmith -v --lifetime 480 | |
| # Use a WebSphere {xor} encoded password (auto-detected) | |
| %(prog)s -k ltpa.keys -p "{xor}CDo9Hgw=" -u jsmith -v | |
| # Decode an existing token | |
| %(prog)s -k ltpa.keys -p mypassword --decode "base64tokenhere" --version 1 | |
| """ | |
| ) | |
| parser.add_argument('-k', '--keys', required=True, | |
| help='Path to ltpa.keys file') | |
| parser.add_argument('-p', '--password', required=True, | |
| help='Password used to encrypt the LTPA keys. Supports {xor} encoded passwords (auto-detected)') | |
| parser.add_argument('-u', '--user', | |
| help='Username for the token (e.g., jsmith or uid=jsmith,ou=users,dc=example,dc=com)') | |
| parser.add_argument('--version', choices=['1', '2', 'both'], default='both', | |
| help='LTPA version to generate (default: both)') | |
| parser.add_argument('--expiry', type=str, | |
| help='Token expiration time (format: YYYY-MM-DD HH:MM:SS). Default: current time + lifetime') | |
| parser.add_argument('--lifetime', type=int, default=120, | |
| help='Token lifetime in minutes (default: 120). Ignored if --expiry is set') | |
| parser.add_argument('--time-offset', type=int, default=0, | |
| help='Time offset in seconds to add/subtract from current time (for server sync)') | |
| parser.add_argument('-v', '--verbose', action='store_true', | |
| help='Enable verbose/debug output') | |
| parser.add_argument('--decode', type=str, | |
| help='Decode an existing token instead of generating one') | |
| parser.add_argument('--realm', type=str, | |
| help='Override realm from keys file') | |
| args = parser.parse_args() | |
| # Validate arguments | |
| if not args.decode and not args.user: | |
| parser.error("--user is required when generating tokens") | |
| # Check keys file exists | |
| keys_path = Path(args.keys) | |
| if not keys_path.exists(): | |
| print(f"Error: Keys file not found: {args.keys}", file=sys.stderr) | |
| sys.exit(1) | |
| try: | |
| # Check if password is {xor} encoded and show decoded value | |
| is_xor_encoded = args.password.upper().startswith('{XOR}') | |
| if is_xor_encoded: | |
| decoded_password = decode_websphere_password(args.password, verbose=False) | |
| print(f"Detected {{xor}} encoded password") | |
| print(f"Decoded password: {decoded_password}") | |
| print() | |
| # Parse and decrypt keys | |
| if args.verbose: | |
| print("=" * 60) | |
| print("LTPA Token Generator") | |
| print("=" * 60) | |
| keys = LTPAKeysFile(args.keys, args.password, verbose=args.verbose) | |
| # Override realm if specified | |
| if args.realm: | |
| keys.realm = args.realm | |
| if args.verbose: | |
| print(f"[DEBUG] Realm overridden to: {args.realm}") | |
| generator = LTPATokenGenerator(keys, verbose=args.verbose) | |
| # Decode mode | |
| if args.decode: | |
| if args.verbose: | |
| print("\n" + "-" * 60) | |
| print("Decoding token...") | |
| print("-" * 60) | |
| if args.version == '1': | |
| decoded = generator.decode_ltpa1_token(args.decode) | |
| print("\nDecoded LTPA v1 Token:") | |
| else: | |
| # Try LTPA2 first, fall back to v1 | |
| try: | |
| decoded = generator.decode_ltpa2_token(args.decode) | |
| print("\nDecoded LTPA2 Token:") | |
| except Exception: | |
| decoded = generator.decode_ltpa1_token(args.decode) | |
| print("\nDecoded LTPA v1 Token:") | |
| for key, value in decoded.items(): | |
| if key != 'raw': | |
| print(f" {key}: {value}") | |
| if args.verbose: | |
| print(f" raw: {decoded['raw']}") | |
| sys.exit(0) | |
| # Calculate expiration time | |
| if args.expiry: | |
| expiration = parse_datetime(args.expiry) | |
| else: | |
| base_time = datetime.now() | |
| if args.time_offset: | |
| base_time = datetime.fromtimestamp(base_time.timestamp() + args.time_offset) | |
| expiration = base_time + timedelta(minutes=args.lifetime) | |
| if args.verbose: | |
| print("\n" + "-" * 60) | |
| print("Token Generation Parameters") | |
| print("-" * 60) | |
| print(f" User: {args.user}") | |
| print(f" Realm: {keys.realm}") | |
| print(f" Expiration: {expiration}") | |
| print(f" Time offset: {args.time_offset} seconds") | |
| # Generate tokens | |
| print("\n" + "=" * 60) | |
| if args.version in ['1', 'both']: | |
| token_v1 = generator.generate_ltpa1_token(args.user, expiration) | |
| print("LtpaToken (v1):") | |
| print(token_v1) | |
| if args.verbose: | |
| print("\nVerification - decoding generated v1 token:") | |
| decoded = generator.decode_ltpa1_token(token_v1) | |
| for key, value in decoded.items(): | |
| if key != 'raw': | |
| print(f" {key}: {value}") | |
| if args.version in ['2', 'both']: | |
| if args.version == 'both': | |
| print("\n" + "-" * 60) | |
| token_v2 = generator.generate_ltpa2_token(args.user, expiration) | |
| print("LtpaToken2 (v2):") | |
| print(token_v2) | |
| if args.verbose: | |
| print("\nVerification - decoding generated v2 token:") | |
| decoded = generator.decode_ltpa2_token(token_v2) | |
| for key, value in decoded.items(): | |
| if key != 'raw': | |
| print(f" {key}: {value}") | |
| print("=" * 60) | |
| # Print cookie usage hint | |
| print("\nUsage as cookies:") | |
| if args.version in ['1', 'both']: | |
| print(f" Cookie: LtpaToken={token_v1}") | |
| if args.version in ['2', 'both']: | |
| print(f" Cookie: LtpaToken2={token_v2}") | |
| except Exception as e: | |
| print(f"Error: {e}", file=sys.stderr) | |
| if args.verbose: | |
| import traceback | |
| traceback.print_exc() | |
| sys.exit(1) | |
| if __name__ == '__main__': | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment