<?php
// =============================================================================
// crypto.php — PHP port of auth_signature.py and bootstrap_codec.py
//
// All routines accept the auth key as the first argument so constants stay
// in the config row, not baked into source.
// =============================================================================

// ---------------------------------------------------------------------------
// libapksec signing — gs / bs / hm
// ---------------------------------------------------------------------------

/**
 * gs(pkg, cert_hex, ts) -> first 16 bytes of
 *     SHA-256( auth_key_raw || pkg || ':' || cert_hex || ':' || ts )
 * as 32 hex chars. Used in /api/guard/key POST `s` field.
 */
function sig_gs($auth_key_raw, $pkg, $cert_hex, $ts) {
    $preimage = $auth_key_raw . $pkg . ':' . $cert_hex . ':' . $ts;
    $digest = hash('sha256', $preimage, true);
    return bin2hex(substr($digest, 0, 16));
}

/**
 * bs(a, b, c) -> HMAC-SHA256( hex(auth_key)_ascii, c.b.a ).hex()  (arg order reversed)
 */
function sig_bs($auth_key_raw, $a, $b, $c) {
    $key = bin2hex($auth_key_raw);
    $msg = $c . '.' . $b . '.' . $a;
    return hash_hmac('sha256', $msg, $key);
}

/**
 * hm(s1, s2, body) -> HMAC-SHA256( hex(auth_key)_ascii, s1.s2.body_raw ).hex()
 * The `.` separator is always present, even when body is empty.
 */
function sig_hm($auth_key_raw, $s1, $s2, $body_raw) {
    $key = bin2hex($auth_key_raw);
    $msg = $s1 . '.' . $s2 . '.' . $body_raw;
    return hash_hmac('sha256', $msg, $key);
}

// ---------------------------------------------------------------------------
// /api/guard/key response builder
// ---------------------------------------------------------------------------

/**
 * Given the 16-byte target server_key the client should install,
 * return the 16-byte payload to put in {"k": <hex>}. The client computes:
 *     server_key[i] = auth_key[i] XOR resp[i]
 */
function sig_sk_response_for_target($auth_key_raw, $target_server_key) {
    $out = '';
    $n = min(strlen($auth_key_raw), strlen($target_server_key));
    for ($i = 0; $i < $n; $i++) {
        $out .= chr(ord($auth_key_raw[$i]) ^ ord($target_server_key[$i]));
    }
    return $out;
}

// ---------------------------------------------------------------------------
// Bootstrap (PanelBootstrap, dormant in this build but supported)
// ---------------------------------------------------------------------------

/**
 * Verifies the request signature `s` from /api/guard/bootstrap.
 *   s = HMAC-SHA256(auth_key_raw, t.n.b).hex()
 */
function bootstrap_verify_signature($auth_key_raw, $ts, $nonce, $build_id, $s) {
    $msg = $ts . '.' . $nonce . '.' . $build_id;
    $expected = hash_hmac('sha256', $msg, $auth_key_raw);
    return hash_equals($expected, (string)$s);
}

/**
 * Build the encrypted payload returned by /api/guard/bootstrap.
 *   aes_key = HMAC-SHA256(auth_key_raw, nonce).digest()      // 32 bytes
 *   iv      = random 16 bytes
 *   ct      = AES-256-CBC(aes_key, iv).encrypt(pad(json))
 *   payload_hex = bin2hex(iv . ct)
 */
function bootstrap_encrypt_payload($auth_key_raw, $nonce, $inner_array) {
    $aes_key = hash_hmac('sha256', $nonce, $auth_key_raw, true);
    $iv = random_bytes(16);
    $plain = json_encode($inner_array, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    $ct = openssl_encrypt($plain, 'aes-256-cbc', $aes_key, OPENSSL_RAW_DATA, $iv);
    if ($ct === false) {
        throw new RuntimeException('AES encrypt failed');
    }
    return bin2hex($iv . $ct);
}

function bootstrap_decrypt_payload($auth_key_raw, $nonce, $payload_hex) {
    $raw = @hex2bin($payload_hex);
    if ($raw === false || strlen($raw) < 32) {
        return null;
    }
    $iv = substr($raw, 0, 16);
    $ct = substr($raw, 16);
    $aes_key = hash_hmac('sha256', $nonce, $auth_key_raw, true);
    $plain = openssl_decrypt($ct, 'aes-256-cbc', $aes_key, OPENSSL_RAW_DATA, $iv);
    if ($plain === false) {
        return null;
    }
    $obj = json_decode($plain, true);
    return is_array($obj) ? $obj : null;
}

// ---------------------------------------------------------------------------
// Self-test helper — call from a test script to verify against Python vectors
// ---------------------------------------------------------------------------

// ---------------------------------------------------------------------------
// libapksec ns() encrypt — inverse of the binary's ns() decrypt.
//
// Algorithm (matches IDA-reversed _decrypt_static + apksec.c):
//
//   working_key = key_part_a XOR key_part_b   (when /key has NOT run; this is
//                                              what the binary uses by default)
//   K_aes  = SHA-256( expected_cert(32) || working_key(16) || iv(16) )
//   cbc_iv = random 16 bytes -> placed at ct[0..15]
//   plain  = url_utf8, padded PKCS#7 to 16-byte multiple
//   cbody  = AES-256-CBC.encrypt(K_aes, cbc_iv, plain)
//   ct     = cbc_iv(16) || cbody             (RESELLER FORMAT — no 32-byte tail;
//                                              matches libapksec_reseller's ns())
//   iv_passed = iv  (the second arg to Guard.ns)
//
// Returns ['ct_hex' => ..., 'iv_hex' => ..., 'literal' => 'ct_hex|iv_hex'].
// ---------------------------------------------------------------------------
function ns_encrypt_url($expected_cert_hex, $working_key_hex, $url) {
    $expected_cert = @hex2bin($expected_cert_hex);
    $working_key   = @hex2bin($working_key_hex);
    if ($expected_cert === false || strlen($expected_cert) !== 32) {
        throw new InvalidArgumentException('expected_cert_hex must be 32 raw bytes (64 hex chars)');
    }
    if ($working_key === false || strlen($working_key) !== 16) {
        throw new InvalidArgumentException('working_key_hex must be 16 raw bytes (32 hex chars)');
    }
    if (!is_string($url) || $url === '') {
        throw new InvalidArgumentException('url required');
    }

    $iv_passed = random_bytes(16);
    $cbc_iv    = random_bytes(16);

    // K_aes
    $K = hash('sha256', $expected_cert . $working_key . $iv_passed, true);

    // PKCS#7-pad url then AES-256-CBC encrypt
    $plain = $url;
    $pad   = 16 - (strlen($plain) % 16);
    $plain .= str_repeat(chr($pad), $pad);

    $cbody = openssl_encrypt($plain, 'aes-256-cbc', $K, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, $cbc_iv);
    if ($cbody === false) {
        throw new RuntimeException('openssl_encrypt failed: ' . openssl_error_string());
    }

    // Reseller format: no random tail. The b3studios SO drops 32 trailing bytes,
    // but libapksec_reseller doesn't expect them, so we skip them — saves
    // ~64 hex chars per literal, much less likely to wrap or corrupt on paste.
    $ct = $cbc_iv . $cbody;

    return [
        'ct_hex'  => bin2hex($ct),
        'iv_hex'  => bin2hex($iv_passed),
        'literal' => bin2hex($ct) . '|' . bin2hex($iv_passed),
    ];
}

function crypto_self_test() {
    $auth_key = hex2bin('32b1df934bc7145921896123707f1b35');
    $pkg = 'com.pxqmwqwr.ultra';
    $cert = '2e6a938305a497761731a6287141bbc67e2e22562911e31ff86c91cd1e92a521';

    $ok = true;
    $expected = '9dcad5a3e4e8ffb249b64214006a2e9a';
    $got = sig_gs($auth_key, $pkg, $cert, '1780716657');
    if ($got !== $expected) { $ok = false; echo "gs FAIL: $got != $expected\n"; }

    $expected = '36e1e3c4e34b1c388d51b323e3835627f677c5e1d9fefe08f07f8fd39e519c31';
    $got = sig_bs($auth_key, 'aaa', 'bbb', 'ccc');
    if ($got !== $expected) { $ok = false; echo "bs FAIL: $got != $expected\n"; }

    $expected = 'a75f91f5b01ff6903b33320d044039c8808e49ee98dbccf1c1a661717fa55b47';
    $got = sig_hm($auth_key, '1780717426', 'e3eb728891cdc2d4565be7042761beaf', '');
    if ($got !== $expected) { $ok = false; echo "hm FAIL: $got != $expected\n"; }

    return $ok;
}
