В предыдущей статье я рассказывал о своём приложении позволяющем мониторить уровень сигнала и тип интернета в смартфонах и некоторых моделях роутеров, работающих c мобильным интернетом. В опросе к той статье со счётом (8 : 2) победило мнение, описывать с кодом на Java добавление новых роутеров в приложение.

Как и планировал, приобрёл Huawei B535-232a. Подержанный, с одной антеннкой, потёртый корпус, слегка глючный, но работающий.

То что роутер мне достался с одной антенной это не беда, у него полноценный разъем для антенны, работающей и на приём и на передачу тоже всего один, MAIN. Второй DIV вспомогательный, работает только на приём. Работать будет и с одной антеннкой. Для того чтоб забрать из роутера данные о текущем типе сети и уровне сигнала RSRP и RSSI, нам нужно авторизоваться в роутере, повторяя поведение браузера, подглядывая в его js скрипты.

Huawei B535-232a
Huawei B535-232a

Авторизация в Huawei B535-232a выглядит как комбо из кинетика и модема E3372h. И похоже у этого комбо есть название: SCRAM (Salted Challenge Response Authentication Mechanism).

Роутер не будет вам отвечать, если вы не присылаете никакие cookie в запросах, поэтому первым делом просим куки, делая GET запрос какому-нибудь endpoint API, например к статусу.

 Request request = new Request.Builder()
                .url(HTTP + router.getAddress() + "/api/monitoring/status")
                .header(HOST, router.getAddress())
                .header(RESPONSE_SOURCE, BROSWER)
                .header(REFERER, HTTP + router.getAddress() + "/")
                .header(X_REQUESTED_WITH, XML_HTTP_REQUESTED)
                .header("Update-Cookie", "UpdateCookie")
                .header(USER_AGENT, MOBILE_USER_AGENT)
                .build();

Тут главное заголовок Update-Cookie, так мы просим дать нам гостевую куку. Сразу после получения ответа, можно попросить у роутера токен, с помощью которого мы позже будем делать запрос.

Request request = new Request.Builder()
                .url(HTTP + router.getAddress() + "/api/webserver/token")
                .header(HOST, router.getAddress())
                .header(RESPONSE_SOURCE, BROSWER)
                .header(REFERER, HTTP + router.getAddress() + "/")
                .header(X_REQUESTED_WITH, XML_HTTP_REQUESTED)
                .header(USER_AGENT, MOBILE_USER_AGENT)
                .build();

Ответы роутер присылает в виде XML, мы просто парсим нужные данные из тегов. Токен состоит из 64 hex символов.

"<token>El0IHnhJq0A7z0NHk56106bEg0XUBY9bJ14XfURzQmk4hzwG0h82VLRQ641PedXx</token>"

64 символа это много, куда столько, не удобно читать. Выкидываем первые 32 и оставляем заднюю часть.

все всегда любят именно заднюю часть, там много мяса и мало костей. (к/ф Гараж 1979г)

Генерируем сами рандомную строку из 64 hex символов, для отправки в качестве firstnonce в первом шаге рукопожатия. Отрицательно оптимизированный токен отправляем в заголовке __RequestVerificationToken. В качестве логина отправляем admin.

 String firstNonce = generateFirstNonce();
        params.setFirstnonce(firstNonce);

        String xmlBody = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><request><username>admin</username><firstnonce>"
          + firstNonce + "</firstnonce><mode>1</mode></request>";
        RequestBody body = RequestBody.create(xmlBody, MediaType.parse("application/x-www-form-urlencoded; charset=UTF-8"));
        Request request = new Request.Builder()
                .url(HTTP + router.getAddress() + "/api/user/challenge_login")
                .post(body)
                .header(RESPONSE_SOURCE, BROSWER)
                .header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
                .header("__RequestVerificationToken", params.getToken().substring(params.getToken().length() - 32))
                .header(HOST, router.getAddress())
                .header(REFERER, HTTP + router.getAddress() + "/")
                .header("X-Requested-With", "XMLHttpRequest")
                .header(USER_AGENT, MOBILE_USER_AGENT)
                .build();

В ответ мы получаем много всего, в заголовке __RequestVerificationToken ответа приходит новый токен 32 символа, его мы сохраняем для следующего запроса, из тела ответа парсим три значения

  • servernonce: 96 символов, это наш firstnonce плюс хвостик сгенерированный роутером

  • salt: 64 символа

  • iterations: это просто цифра 1000

Теперь нам надо зашифровать, сильно и сложно, с помощью полученных данных наш пароль. Переводим строку salt в массив байт, и шифруем им пароль 1000 раз, как указал роутер.

// private synchronized String calculateClientProof(String password, HuaweiAuthParams params)

byte[] saltBytes = hexToBytes(params.getSalt());
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), saltBytes, params.getIterations(), 256);
byte[] saltedPassword = mSkf.generateSecret(spec).getEncoded();

Дальше наш зашифрованный солью пароль нужно подписать клиентским ключиком и получить из результата хэш в виде массива байт.

// подписываем пароль
byte[] clientKey = hmacSha256("Client Key".getBytes(StandardCharsets.UTF_8), saltedPassword);
// получаем hash
// MessageDigest mDigest = MessageDigest.getInstance("SHA-256");
mDigest.reset();
byte[] storedKey = mDigest.digest(clientKey);


private synchronized byte[] hmacSha256(byte[] key, byte[] data) throws InvalidKeyException {
    //  Mac mMac = Mac.getInstance("HmacSHA256");
        mMac.reset();
        mMac.init(new SecretKeySpec(key, "HmacSHA256"));
        return mMac.doFinal(data);
    }

С помощью нашего firstnonce, который мы отправляли роутеру и servernonce который он прислал в ответ, создаём длиннющую строку 258 символов, склеивая через запятые наши нонсы. И подписываем ею наш хэш пароля.

// строка подписи
String authMessage = params.getFirstnonce() + "," + params.getServernonce() + "," + params.getServernonce();
// подписанный хэш пароля
byte[] clientSignature = hmacSha256(authMessage.getBytes(StandardCharsets.UTF_8), storedKey);

И наконец, мы делаем XOR, накладываем на подписанный клиентской подписью зашифрованный только солью пароль, побайтово с исключающим ИЛИ, подписанный склеенными нонсами хэш пароля. Полученный массив переводим в строку.

for (int i = 0; i < clientKey.length; i++) {
     clientKey[i] ^= clientSignature[i];
    }
    return bytesToHex(clientKey);

Её мы отправим в теле запроса как clientproof вместе с servernonce. В заголовок __RequestVerificationToken положим токен полученный из предыдущего ответа.

  String clientProof = calculateClientProof(router.getPass(), params);

        String xmlBody = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
                "<request><clientproof>" + clientProof + "</clientproof><finalnonce>"
          + params.getServernonce() + "</finalnonce></request>";

        RequestBody body = RequestBody.create(xmlBody, MediaType.parse(CONTENT_TYPE_BODY));
        Request request = new Request.Builder()
                .url(HTTP + router.getAddress() + "/api/user/authentication_login")
                .post(body)
                .header(RESPONSE_SOURCE, BROSWER)
                .header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
                .header("__RequestVerificationToken", params.getRequestVerificationToken())
                .header(HOST, router.getAddress())
                .header(REFERER, HTTP + router.getAddress() + "/")
                .header(X_REQUESTED_WITH, XML_HTTP_REQUESTED)
                .header(USER_AGENT, MOBILE_USER_AGENT)
                .build();

И если вы не перепутали при подписывании пароля ключами, что является ключом, а что сообщением как я, потому что скрипт хуавея кидает ключ вторым параметром, а не первым.

this.cfg.hmac(e,"Client Key")

то в ответ вам сразу, а не через 2 суток, придут 32 токена, два из них в отдельных заголовках __RequestVerificationTokenone и __RequestVerificationTokentwo. Которые я не знаю зачем нужны и не очень интересно, потому что вместе с ними приходят и новые админско-королевские cookie. Которые позволяют запросить данные о сети и сигнале. Запрос делается так же как и на E3372h, к /api/monitoring/status и /api/device/signal

и результат возвращает также как у E3372h. RSSI и RSRP в виде нормальных параметров -51dBm, -89dBm, а тип сети в виде кодов

Скрытый текст
    MACRO_NET_WORK_TYPE_EX_NR: "111",
        MACRO_NET_WORK_TYPE_EX_LTE_PLUS: "1011",
        MACRO_NET_WORK_TYPE_EX_LTE: "101",
        MACRO_NET_WORK_TYPE_EX_802_16E: "81",
        MACRO_NET_WORK_TYPE_EX_TD_HSPA_PLUS: "65",
        MACRO_NET_WORK_TYPE_EX_TD_HSPA: "64",
        MACRO_NET_WORK_TYPE_EX_TD_HSUPA: "63",
        MACRO_NET_WORK_TYPE_EX_TD_HSDPA: "62",
        MACRO_NET_WORK_TYPE_EX_TD_SCDMA: "61",
        MACRO_NET_WORK_TYPE_EX_DC_HSPA_PLUS: "46",
        MACRO_NET_WORK_TYPE_EX_HSPA_PLUS: "45",
        MACRO_NET_WORK_TYPE_EX_HSPA: "44",
        MACRO_NET_WORK_TYPE_EX_HSUPA: "43",
        MACRO_NET_WORK_TYPE_EX_HSDPA: "42",
        MACRO_NET_WORK_TYPE_EX_WCDMA: "41",
        MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_B: "36",
        MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_A: "35",
        MACRO_NET_WORK_TYPE_EX_HYBRID_EHRPD_REL_0: "34",
        MACRO_NET_WORK_TYPE_EX_EHRPD_REL_B: "33",
        MACRO_NET_WORK_TYPE_EX_EHRPD_REL_A: "32",
        MACRO_NET_WORK_TYPE_EX_EHRPD_REL_0: "31",
        MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_B: "30",
        MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_A: "29",
        MACRO_NET_WORK_TYPE_EX_HYBRID_EVDO_REV_0: "28",
        MACRO_NET_WORK_TYPE_EX_HYBRID_CDMA_1X: "27",
        MACRO_NET_WORK_TYPE_EX_EVDO_REV_B: "26",
        MACRO_NET_WORK_TYPE_EX_EVDO_REV_A: "25",
        MACRO_NET_WORK_TYPE_EX_EVDO_REV_0: "24",
        MACRO_NET_WORK_TYPE_EX_CDMA_1X: "23",
        MACRO_NET_WORK_TYPE_EX_IS95B: "22",
        MACRO_NET_WORK_TYPE_EX_IS95A: "21",
        MACRO_NET_WORK_TYPE_EX_EDGE: "3",
        MACRO_NET_WORK_TYPE_EX_GPRS: "2",
        MACRO_NET_WORK_TYPE_EX_GSM: "1",
        MACRO_NET_WORK_TYPE_EX_NOSERVICE: "0",
        MACRO_NET_WORK_TYPE_LTE_NR: "20",
        MACRO_NET_WORK_TYPE_LTE: "19",
        MACRO_NET_WORK_TYPE_HSPA_PLUS_MIMO: "18",
        MACRO_NET_WORK_TYPE_HSPA_PLUS_64QAM: "17",
        MACRO_NET_WORK_TYPE_3XRTT: "16",
        MACRO_NET_WORK_TYPE_1XEVDV: "15",
        MACRO_NET_WORK_TYPE_UMB: "14",
        MACRO_NET_WORK_TYPE_1XRTT: "13",
        MACRO_NET_WORK_TYPE_EVDO_REV_B: "12",
        MACRO_NET_WORK_TYPE_EVDO_REV_A: "11",
        MACRO_NET_WORK_TYPE_EVDO_REV_0: "10",
        MACRO_NET_WORK_TYPE_HSPA_PLUS: "9",
        MACRO_NET_WORK_TYPE_TDSCDMA: "8",
        MACRO_NET_WORK_TYPE_HSPA: "7",
        MACRO_NET_WORK_TYPE_HSUPA: "6",
        MACRO_NET_WORK_TYPE_HSDPA: "5",
        MACRO_NET_WORK_TYPE_WCDMA: "4",
        MACRO_NET_WORK_TYPE_EDGE: "3",
        MACRO_NET_WORK_TYPE_GPRS: "2",
        MACRO_NET_WORK_TYPE_GSM: "1",

Ах если бы

первым у меня оказался B535-232a, мне бы не пришлось ничего делать для E3372h-320, код написанный для роутера спокойно забирает данные и с модема, который не требует такой авторизации. Подробный код можно найти на GitHub, а приложение на Google Play

Спасибо за внимание.

Комментарии (3)


  1. XclusR
    21.04.2026 17:59

    Вроде home assistant тоже умеет забирать данные с модемов. У меня даже получалось Читать смс. По крайней мере с с B315s-22. Это я к тому что если что - можно подсмотреть как там это сделано


    1. JDJ Автор
      21.04.2026 17:59

      да, я при добавлении ещё B593s-22 пробовал смотреть в репозиторий huawei-lte-api, его вроде используем хоум ассистент, но то ли этого роутера не оказалось в поддерживаемых, то ли код не совпадал с тем что делал браузер при подключении, в общем я пошёл путём подглядывания в js скрипты которые прилетают в браузер. А смс вы читаете просто запросом, или какая то опрашивался периодическая?


      1. XclusR
        21.04.2026 17:59

        У меня был удаленный доступ к Home Assistant но не было доступа к веб морде модема, нужно было просто единоразово прочитать код из смс. Я нашел как это сделать через AppDaemon. Сейчас я перешел на роутер Keentic + 4g modem, там через родное приложение Netcarze можно читать смс из коробки