sanghviharshit / ha-snoo

👶💤 Custom component to integrate SNOO smart sleeper from www.happiestbaby.com into home assistant. Use HACS for easy install and upgrades
9 stars 2 forks source link

Login API Endpoint Change #5

Open droans opened 4 months ago

droans commented 4 months ago

For the past couple weeks, I've been receiving an error in my logs that the login endpoint cannot be reached.

Message:

ERROR (MainThread) [pysnooapi.api] Authentication failed: Error requesting data from https://snoo-api.happiestbaby.com/us/v2/login: 404 - Not Found

Using PCAPDroid with a recompiled APK, it appears the proper endpoint is https://cognito-idp.us-east-1.amazonaws.com with the data being:

'{"AuthParameters": {"PASSWORD": "<PASSWORD>", "USERNAME": "<EMAIL>"}, "AuthFlow": "USER_PASSWORD_AUTH", "ClientId": "6kqofhc8hm394ielqdkvli0oea"}'

Headers:

{
    'x-amz-target': 'AWSCognitoIdentityProviderService.InitiateAuth', 
    'content-type': 'application/x-amz-json-1.1', 
    'accept-language': 'US', 
    'accept-encoding': 'gzip', 
    'user-agent': 'okhttp/4.12.0', 
    'accept': 'application/json'
}

Which will then return a Bearer Token to login in the format of:

{
    "AuthenticationResult": {
        "AccessToken": "<VERY_LONG_TOKEN>", 
        "TokenType": "Bearer"
    },
    "ChallengeParameters": {}
}

I'm not sure if this is any different than before, but you then need to get a second token. It's retrieved by sending a POST request to https://api-us-east-1-prod.happiestbaby.com/us/me/v10/pubnub/authorize with the following headers and data:

Headers:

{
    'accept-language': 'US',
     'authorization': 'Bearer '<VERY_LONG_TOKEN_FROM_ABOVE>, 
    'content-type': 'application/json; charset=UTF-8', 
    'accept-encoding': 'gzip', 
    'user-agent': 'okhttp/4.12.0', 
    'accept': 'application/json'
}

Data:

{
    'advertiserId': '', 
    'appVersion': '1.8.7', 
    'device': 'panther', 
    'deviceHasGSM': true, 
    'locale': 'en', 
    'os': 'Android', 
    'osVersion': '14', 
    'platform': 'Android', 
    'timeZone': 'America/New_York', 
    'userCountry': 'US', 
    'vendorId': 'eyqurgwYQSqmnExnzyiLO5'
}

And you'll receive data similar to:

{
    "snoo": {
        "token": "<ANOTHER_LONG_TOKEN>",
        "serialNumbers": ["<DEVICE_SERIAL>"]
    },
    "expiresIn":10800
}

The token is then passed as a parameter in the requests. To retrieve the general status, send a GET request to https://happiestbaby.pubnubapi.com/v2/subscribe/sub-c-97bade2a-483d-11e6-8b3b-02ee2ddab7fe/ActivityState.<DEVICE_SERIAL>/0?tt=<TIMESTAMP-17200321439663207>&pnsdk=PubNub-Kotlin%2F7.4.0&l_pub=0.091&auth=<ANOTHER_LONG_TOKEN>&heartbeat=300&requestid=<UUID>&state=%7B%7D&uuid=<PHONE_UUID>&tr=24

UUID is formatted as XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX. PHONE_UUID is formatted similarly but is prefixed with the phone type plus a 23 character hex, so something like android_XXXXXXXXXXXXXXXXXXXXXXX_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX

McCloudS commented 4 months ago

Messed with this a little bit and couldn't get very far.

curl -X POST https://cognito-idp.us-east-1.amazonaws.com \
-H 'Content-Type: application/x-amz-json-1.1' \
-H 'X-Amz-Target: AWSCognitoIdentityProviderService.InitiateAuth' \
-d '{
    "AuthParameters": {
        "USERNAME": "<email@gmail.com>",
        "PASSWORD": "<password>"
    },
    "AuthFlow": "USER_PASSWORD_AUTH",
    "ClientId": "6kqofhc8hm394ielqdkvli0oea"
}'

got me the JSON response of the AccessToken, IdToken, and RefreshToken, but couldn't get past that. (Truncated example below):

    "AuthenticationResult": {
        "AccessToken": "eyJraWQiOiJZSW8zUUNDVGZ3TnNYRjIxSGxwYWZGMlQ4VXk2TGJ1OVl4aEtpK29oS0VFPSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiI3ODg0OWEyNi0xNTc2LTQxMjctYjNhNS1iYjliM2I1NmUxNDAiLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAudXMtZWFzdC0xLmFtYXpvbmF3cy5jb21cL3VzLWVhc3QtMV9XMUNESHZOV2kiLCJjbGllbnRfaWQiOiI2a3FvZmhjOGhtMzk0aWVscWRrdmxpMG9lYSIsIm9yaWdpbl9qdGkiOiJhNzcxM2Y0NC01NTBmLTRiNGYtYjkwNC05NDFmNjRkNWVjNTAiLCJldmVudF9pZCI6ImU4NDRhMTAwLWRhNTgDkzWrJvFAKc1joBcwRN0BSYrNEYwwJU8Io0JgEmSwJcUc5KY82ihlIMJm4IUXJXnCkL7kHE6K7YXraQkNA0jD_tMqESU4If13cap72W4FHDAFcPChi9yHcPJkJYTtRs291Fp3Bchb-spVeUxsz2oyNkFrJOanOtDEXVu5QneDLQjhzz-si8ObiodQvdbjJxKub3n3LxWZpJwcGaSdgm3oI2-2tWLBcpAuAs2vDXC82i0sW76hZecEjF9Mbvb-ooaNO97x9ZPMN-ZE91Bm5QObExTuJl5BSEaYzx6vca9JI_3UwqjUd9QWnUr6jp_kn46YaPDSySFvUx1w",
        "ExpiresIn": 10800,
        "IdToken": "eyJraWQiOiJqM1wveklKSTFCQUhwUml5UWJTRzdDZmpRNXZPNnRaWStyUG9XUGFWdXRBND0iLCJhbGciOiJSUzI1NiJ9.eyJmaWQiOiI1ZWExZTNlMzQ1NGU5ZjBlNmVlNmI2ZjQiLCJzdWIiOiI3ODg0OWEyNi0xNTc2LTQxMjctYjNhNS1iYjliM2I1NmUxNDAiLCJ2ZXIiOiIyIiwic2JzIjoiRlJFRV9QUkVNSVVNIiwiaXNzIjoiaHR0cHM6XC9cL2NvZ25pdG8taWRwLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tXC91cy1lYXN0LTFfVzFDREh2TldpIiwiY29nbml0bzp1c2VybmFtZSI6Ijc4ODQ5YTI2LTE1NzYtNDEyNy1iM2E1LWJiOWIzYjU2ZTE0MCIsImR2Y3MiOiIyNzg3MjAwOTM3NzE0MTgyOjEiLCJvcmlnaW5fanRpIjoiYTc3MTNmNDQtNTUwZi00YjRmLWI5MDQtOTQxZjY0ZDDQtYmRiZS1kNWMyMzI4MTdlZTQiLCJ0b2tlbl91c2UiOiJpZCIsImF1dGhfdGltZSI6MTcyMDEyMTYzMSwiZXhwIjoxNzIwMTMyNDMxLCJpYXQiOjE3MjAxMjE2MzEsImp0aSI6IjIyZDM4NDg5LWNjYjgtNDJjMi1iNmE5LWZkZTBmYjNkYjlmZSJ9.fN_79DDNQ8_MWrX9NDg1haL9yEWULl3jRCxA4TGNHvvpubKQVyGylTx32c_3N6cSgUczgmBboxdkkgz27FI2pyEWFEjqdz1f1MmWsLFpDpiIvVeElXQReF8fpTz8qrL2Cn_ezB6tfXoJdX2iPOmqjymuOPcoNkrhPnWl6qC6rU4IhVCJTpKqDve7ecnD0ySZb5oNJ8Cddh4wRS4FLrW1JEttHUat4XrDaiteOw5O3lD3M_Im3Ld4NdpeT_u_EMh4L-u8GiAGPONAsnXgNiERvIn2Ai26UlNywxH2mck05pm5IFNSBY592NXk9KCZmlFps-4_z5CyP0Qed9WyJrjb2Q",
        "RefreshToken": "eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAifQ.X5qhiNg_HrS1_G_R2Kk7COHP5Cf5YVsMIU9HnbbKGWfh8RaGNh59UBpNhwgCXquaq_EjltuCptE4luZPyZPLaUJCPdc5PheeTqcMzvGfLJO8F22_Knwqt7bXv-LLMiaRqiHk-SiUIJjZ9_3ve0G-zVGvyKYK2Fmb8E1BaCgzzsqWcRQatcOCgNMTA2UPYQmnmbGXJyI7tRvPEfyCdakL57ncQ2unRNtmOgVkM82iYuYT-eNeh_3E0EOpPcnJTlHMTPq0u-4EdHoymi7NmGeFGoQbe02N_Mx6s_4On8hmq3rkL3E0tUfZxF_7xZsNaC495O1EvVQmY5fKvGkAF7cHHA.yTK7ImhMTcUgNWab.qnxN9qvKxQufwek9HjY3EeJO9MU83yUr750oCADeBGTqrpfyMyB5C5ChEKd8sesLQL400x_5-XWlb3qTmx_HO6R-RvQcYKw070IBA9ubyElv7Nh2y9qqEY0eHp1G5wFDi7fzNfV8z_rVO3RUivDSZbNx9oDRocTzoIgozDrQZYz9hPhu_DYqFR3lq95bGUIlF2IoUCYHekzudOlpbwggdxdC_IcjTY-78GNMSFRhp0D8ayFXjhvQkKy1EGvNK85fsifOHOfhMXPUg9nQoIq2UmcXOYcCwvusR_4d3GnolZ2_YOSd9r4Z_RG5xHJ3ofcgTrxSvdv3iZXg9KUiVyufva_uzYblnd439TB8Z5iRiRAOhNMV95FbGKC7EhLzL-r4n-1FlI-55PxQjZ8vK9od-3mEiqJIVxFt4D54_Dr2DYKNomMloNr_qSkYseWiXxFSUL-zrPYto6T5okq9uqmhvKj_DWOUY62Jokb5HP_Q-8SdUmvN-fEr4Wr1HQ9V29zs6qH9mNu2R-x4_6RETpOo04TU_lV8q_IEoqKQiBQfNwMJ7ad7vYZWa1SHt3TaPD1hxp3u2O-0E9RJmGxUM2JoTc-Pu5aJ-9kSvF-H-z4wzFANn2YRxNZ5V3lf4oxvdcwsYw8JoTLxu2AdK7p4DlVN1YZQR99IENguhIsRHOuL13LsTexXw3bsV7rF-nC1fOgHgWdd4s3htSiJrSY_Kzj_lkyrzfOwnVKTeWpqwNagYVExDervxmmtTg-vJ15gm3jr6VI1kBMj3mlaxZJdCYjoHwhehkPypM4tZtLGE451RxGo4-OYMDxQFpRlO4t0JoQP9UFvyyvS4RyX37o8SFID10n9_ZihqVUBkoRZnD-846mfZIaUUQDnXdUqNgihcjNwdkWnVrp4Nu2cDsnXa1ry9qLqEY4oHTQXH1cwk4ADhKiW874JanPs_W0zJL7pvuEH05PmkASV946USSdvZG4TCJb78txT-84jrrjE8Ptpk3kzAm-kujSxp6OMYuIn_dXLuysUJ-y9-ENYCRBLDMQ1so3-d3tcxHeBIuo5N2B9ekhIvUgMsDMp0VCjVQz4YGJwDVYU7KLcM-6A8xe9-XlqjimdX9makoPffJmvDLAzGHl15c9_UxGFcJkafXMNrexiRA9akNB_qZkGR5UzzwGQuOa7M64IOZmulmGF50r8SkV14jqw.OaNJW468whhcgqoEFKNPig",
        "TokenType": "Bearer"
    },
    "ChallengeParameters": {}
}
curl -X GET https://api-us-east-1-prod.happiestbaby.com/us/me/v10/pubnub/authorize \
-H 'Authorization: Bearer <long access token>' \
-H 'Content-Type: application/json; charset=UTF-8' \
-H 'Accept-Encoding: gzip' \
-H 'User-Agent: okhttp/4.12.0' \
-H 'Accept: application/json' \
-H 'accept-language: US'

and any variations return {"message":"Forbidden"}

droans commented 4 months ago

I'm having the same issue now, too, which is odd since it worked perfectly for me the other day.

I'm not certain what the issue is... I still have the Python terminal open from when I tested this and I confirmed that I used the exact same data except for the key itself.

I tested a few different endpoints before I got the correct endpoint. I wonder if that might be related. Let me run a couple tests and I'll try to get a basic Python PoC script.

droans commented 4 months ago

Also, if you would like to get the data on your own to figure it out, here are the basic steps. You will need a Linux computer and an Android phone.

  1. Install PCADdroid from F-Droid
  2. Open the app and click the cog icon at the top. Select "TLS decryption". It'll have you install a helper app and a root certificate.

Now, since the app uses a pinned cert, it won't let us use that cert yet. Instead, we have to pull it from the phone, decompile the app and recompile it without pinning, and then reinstall it. Luckily, there are tools to help already.

  1. Download SAI from F-Droid. Click "Backup" and select the Happiest Baby app. Choose a destination.
  2. On your computer, install apk-mitm. You can just run npm install -g apk-mitm.
  3. Take the backup copy of the app's apks file and get it onto your computer. You can either use adb pull or send it to yourself another way.
  4. On your computer, run apk-mitm <name-of-app>. It will perform all the hard work for you.
  5. Once finished, download the patched apks file back onto your phone.
  6. Uninstall the original Happiest Baby app and install the patched copy.

Go back to PCAPdroid and click Ready. Open the Happiest Baby app, login, and then go back to PCAPdroid and click the stop button. Go over to Connections and ensure that the traffic was recorded and decrypted. Now you can click on each connection to view all the packets sent and received.

droans commented 4 months ago

I think I figured out the issue. It turns out it was the idToken that is used.

Here's the code. The authorize() function will return a requests object.

I've also found the endpoints for interacting with the Snoo - starting, stopping, and changing the level. The example code at the bottom should make things clear.

E: I have updated the code to include a Pyscript script and HA templates for people to manually create a template sensor and select to manage their Snoo until the API is fixed.