В предыдущей статье я рассказывал о своём приложении позволяющем мониторить уровень сигнала и тип интернета в смартфонах и некоторых моделях роутеров, работающих c мобильным интернетом. В опросе к той статье со счётом (8 : 2) победило мнение, описывать с кодом на Java добавление новых роутеров в приложение.
Как и планировал, приобрёл Huawei B535-232a. Подержанный, с одной антеннкой, потёртый корпус, слегка глючный, но работающий.
То что роутер мне достался с одной антенной это не беда, у него полноценный разъем для антенны, работающей и на приём и на передачу тоже всего один, MAIN. Второй DIV вспомогательный, работает только на приём. Работать будет и с одной антеннкой. Для того чтоб забрать из роутера данные о текущем типе сети и уровне сигнала RSRP и RSSI, нам нужно авторизоваться в роутере, повторяя поведение браузера, подглядывая в его js скрипты.

Авторизация в 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
Спасибо за внимание.
XclusR
Вроде home assistant тоже умеет забирать данные с модемов. У меня даже получалось Читать смс. По крайней мере с с B315s-22. Это я к тому что если что - можно подсмотреть как там это сделано
JDJ Автор
да, я при добавлении ещё B593s-22 пробовал смотреть в репозиторий huawei-lte-api, его вроде используем хоум ассистент, но то ли этого роутера не оказалось в поддерживаемых, то ли код не совпадал с тем что делал браузер при подключении, в общем я пошёл путём подглядывания в js скрипты которые прилетают в браузер. А смс вы читаете просто запросом, или какая то опрашивался периодическая?
XclusR
У меня был удаленный доступ к Home Assistant но не было доступа к веб морде модема, нужно было просто единоразово прочитать код из смс. Я нашел как это сделать через AppDaemon. Сейчас я перешел на роутер Keentic + 4g modem, там через родное приложение Netcarze можно читать смс из коробки