Skip to content

Commit

Permalink
Optimize capture verification method in China region
Browse files Browse the repository at this point in the history
  • Loading branch information
Yixi committed Sep 7, 2023
1 parent 6e97a82 commit 4c113cd
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 17 deletions.
13 changes: 3 additions & 10 deletions bimmer_connected/api/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
create_s256_code_challenge,
generate_cn_nonce,
generate_token,
get_capture_position,
get_correlation_id,
handle_httpstatuserror,
)
Expand Down Expand Up @@ -284,16 +285,8 @@ async def _login_china(self):
)
verify_id = captcha_res.json()["data"]["verifyId"]

for i in range(1, 13):
try:
captcha_check_res = await client.post(
AUTH_CHINA_CAPTCHA_CHECK_URL,
json={"position": 0.74 + i / 100, "verifyId": verify_id},
)
if captcha_check_res.status_code == 201:
break
except MyBMWAPIError:
continue
position = get_capture_position(captcha_res.json()["data"]["backGroundImg"])
await client.post(AUTH_CHINA_CAPTCHA_CHECK_URL, json={"position": position, "verifyId": verify_id})

# Get token
response = await client.post(
Expand Down
40 changes: 40 additions & 0 deletions bimmer_connected/api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import base64
import datetime
import hashlib
import io
import json
import logging
import mimetypes
Expand All @@ -17,6 +18,7 @@
from Crypto.Hash import SHA256
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad
from PIL import Image

from bimmer_connected.models import AnonymizedResponse, MyBMWAPIError, MyBMWAuthError, MyBMWQuotaError

Expand Down Expand Up @@ -185,3 +187,41 @@ def generate_cn_nonce(username: str) -> str:
aes_hex = cipher_aes.encrypt(pad(phone_text.encode(), AES.block_size)).hex()

return k2 + i1 + sha256_a + aes_hex + k1 + i2 + sha256_b


def get_capture_position(base64_background_img: str) -> str:
"""Get the position of the capture in the background image."""
target_color = [220, 230, 221]
tolerance = 15
block = {"width": 15, "height": 75}

img_bytes = io.BytesIO(base64.b64decode(base64_background_img))
img = Image.open(img_bytes)
pixels = list(img.getdata())

position = ""
found_block = False

for y in range(0, img.height - block["height"]):
for x in range(0, img.width - block["width"]):
found_block = True
for i in range(block["height"]):
for j in range(block["width"]):
pixel_index = (y + i) * img.width + (x + j)
pr = pixels[pixel_index][:3]
dr = abs(pr[0] - target_color[0])
dg = abs(pr[1] - target_color[1])
db = abs(pr[2] - target_color[2])

if dr > tolerance or dg > tolerance or db > tolerance:
found_block = False
break
if not found_block:
break
if found_block:
position = str(round((x - 26) / img.width, 2))
break
if found_block:
break

return position
7 changes: 2 additions & 5 deletions bimmer_connected/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,8 @@ def add_login_routes(self) -> None:
200, json=load_response(RESPONSE_DIR / "auth" / "auth_slider_captcha.json")
)

self.post("/eadrax-coas/v1/cop/check-captcha").mock(
side_effect=[
httpx.Response(422),
httpx.Response(201, json=load_response(RESPONSE_DIR / "auth" / "auth_slider_captcha_check.json")),
]
self.post("/eadrax-coas/v1/cop/check-captcha").respond(
200, json=load_response(RESPONSE_DIR / "auth" / "auth_slider_captcha_check.json")
)

self.post("/eadrax-coas/v2/login/pwd").respond(
Expand Down

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion bimmer_connected/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
import respx
import time_machine

from bimmer_connected.api.utils import get_capture_position
from bimmer_connected.models import ChargingSettings, ValueWithUnit
from bimmer_connected.utils import MyBMWJSONEncoder, get_class_property_names, parse_datetime

from . import VIN_G26
from . import RESPONSE_DIR, VIN_G26, load_response
from .conftest import prepare_account_with_vehicles


Expand Down Expand Up @@ -112,3 +113,10 @@ def test_charging_settings():
assert cs.chargingTarget == 90
assert cs.dcLoudness is None
assert cs.isUnlockCableActive is None


def test_get_capture_position():
"""Test the auto get slider captcha position."""
base64_background_img = load_response(RESPONSE_DIR / "auth" / "auth_slider_captcha.json")["data"]["backGroundImg"]
position = get_capture_position(base64_background_img)
assert position == "0.81"
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
httpx
pycryptodome>=3.4
pyjwt>=2.1.0
Pillow>=6.2.0

0 comments on commit 4c113cd

Please sign in to comment.