Skip to content

Commit

Permalink
remove support for China
Browse files Browse the repository at this point in the history
Signed-off-by: Mark Herwege <[email protected]>
  • Loading branch information
mherwege committed Feb 10, 2025
1 parent a575c5b commit b0453e8
Show file tree
Hide file tree
Showing 10 changed files with 21 additions and 255 deletions.
5 changes: 3 additions & 2 deletions bundles/org.openhab.binding.mybmw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Please note **this isn't a real-time binding**.
If a door is opened the state isn't transmitted and changed immediately.
It's not a flaw in the binding itself because the state in BMW's own MyBMW App is also updated with some delay.

This binding does not support China.

## Supported Things

### Bridge
Expand Down Expand Up @@ -84,10 +86,9 @@ Properties will be attached to predefined vehicles if the VIN is matching.
| password | text | MyBMW Password |
| region | text | Select region in order to connect to the appropriate BMW server. |

The region Configuration has 3 different options
The region Configuration has 2 different options

- _NORTH_AMERICA_
- _CHINA_
- _ROW_ (Rest of World)

At first initialization, follow the online instructions for login into the BMW API.
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,14 @@

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.time.Instant;
import java.util.Base64;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;

import javax.crypto.Cipher;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
Expand All @@ -41,9 +36,6 @@
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.UrlEncoded;
import org.openhab.binding.mybmw.internal.MyBMWBridgeConfiguration;
import org.openhab.binding.mybmw.internal.dto.auth.ChinaPublicKeyResponse;
import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenExpiration;
import org.openhab.binding.mybmw.internal.dto.auth.ChinaTokenResponse;
import org.openhab.binding.mybmw.internal.dto.auth.OAuthSettingsQueryResponse;
import org.openhab.binding.mybmw.internal.handler.MyBMWBridgeHandler;
import org.openhab.binding.mybmw.internal.handler.backend.JsonStringDeserializer;
Expand All @@ -67,6 +59,7 @@
* @author Bernd Weymann - Initial contribution
* @author Martin Grassl - extracted from myBmwProxy
* @author Mark Herwege - refactor to use OAuthFactory
* @author Mark Herwege - remove China
*/
@NonNullByDefault
public class MyBMWTokenController {
Expand Down Expand Up @@ -127,28 +120,20 @@ public synchronized AccessTokenResponse getToken() {
// reset the token as it times out
bridgeHandler.setHCaptchaToken(Constants.EMPTY);
} else if (!waitingForInitialToken && tokenResponse.isExpired(Instant.now(), 5)) {
if (REGION_CHINA.equals(bridgeConfiguration.getRegion())) {
// in China no hcaptchatoken is required, so no need to wait for reinitialization
boolean tokenUpdateSuccess = getAndRefreshTokenChina();
if (!tokenUpdateSuccess) {
logger.warn("Updating token failed!");
}
} else {
// try to refresh the token
boolean tokenUpdateSuccess = refreshTokenROW();
logger.trace("update token {}", tokenUpdateSuccess ? "success" : "failed");

if (!tokenUpdateSuccess) {
logger.warn("Updating token failed!");
waitingForInitialToken = true;

if (bridgeConfiguration.getHCaptchaToken().isBlank()) {
logger.warn(
"initial Authentication failed, request a new captcha token, see https://bimmer-connected.readthedocs.io/en/stable/captcha.html!");
bridgeHandler.tokenInitError();
} else {
getToken();
}
// try to refresh the token
boolean tokenUpdateSuccess = refreshTokenROW();
logger.trace("update token {}", tokenUpdateSuccess ? "success" : "failed");

if (!tokenUpdateSuccess) {
logger.warn("Updating token failed!");
waitingForInitialToken = true;

if (bridgeConfiguration.getHCaptchaToken().isBlank()) {
logger.warn(
"initial Authentication failed, request a new captcha token, see https://bimmer-connected.readthedocs.io/en/stable/captcha.html!");
bridgeHandler.tokenInitError();
} else {
getToken();
}
}
}
Expand Down Expand Up @@ -336,88 +321,4 @@ private String codeFromUrl(String encodedUrl) {
});
return codeFound.toString();
}

/**
* @return true if the token was successfully updated
*/
private boolean getAndRefreshTokenChina() {
try {
/**
* Step 1) get public key
*/
String publicKeyUrl = "https://" + EADRAX_SERVER_MAP.get(REGION_CHINA) + CHINA_PUBLIC_KEY;
Request oauthQueryRequest = httpClient.newRequest(publicKeyUrl);
oauthQueryRequest.header(HttpHeader.USER_AGENT, USER_AGENT);
oauthQueryRequest.header(HEADER_X_USER_AGENT, String.format(X_USER_AGENT, BRAND_BMW,
APP_VERSIONS.get(bridgeConfiguration.getRegion()), bridgeConfiguration.getRegion()));
ContentResponse publicKeyResponse = oauthQueryRequest.send();
if (publicKeyResponse.getStatus() != 200) {
throw new HttpResponseException("URL: " + oauthQueryRequest.getURI() + ", Error: "
+ publicKeyResponse.getStatus() + ", Message: " + publicKeyResponse.getContentAsString(),
publicKeyResponse);
}
ChinaPublicKeyResponse pkr = JsonStringDeserializer
.deserializeString(publicKeyResponse.getContentAsString(), ChinaPublicKeyResponse.class);

/**
* Step 2) Encode password with public key
*/
// https://www.baeldung.com/java-read-pem-file-keys
String publicKeyStr = pkr.data.value;
String publicKeyPEM = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "")
.replaceAll(System.lineSeparator(), "").replace("-----END PUBLIC KEY-----", "").replace("\\r", "")
.replace("\\n", "").trim();
byte[] encoded = Base64.getDecoder().decode(publicKeyPEM);
X509EncodedKeySpec spec = new X509EncodedKeySpec(encoded);
KeyFactory kf = KeyFactory.getInstance("RSA");
PublicKey publicKey = kf.generatePublic(spec);
// https://www.thexcoders.net/java-ciphers-rsa/
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encryptedBytes = cipher.doFinal(bridgeConfiguration.getPassword().getBytes());
String encodedPassword = Base64.getEncoder().encodeToString(encryptedBytes);

/**
* Step 3) Send Auth with encoded password
*/
String tokenUrl = "https://" + EADRAX_SERVER_MAP.get(REGION_CHINA) + CHINA_LOGIN;
Request loginRequest = httpClient.POST(tokenUrl);
loginRequest.header(HEADER_X_USER_AGENT, String.format(X_USER_AGENT, BRAND_BMW,
APP_VERSIONS.get(bridgeConfiguration.getRegion()), bridgeConfiguration.getRegion()));
String jsonContent = "{ \"mobile\":\"" + bridgeConfiguration.getUserName() + "\", \"password\":\""
+ encodedPassword + "\"}";
loginRequest.content(new StringContentProvider(jsonContent));
Instant tokenCreatedOn = Instant.now();
ContentResponse tokenResponse = loginRequest.send();
if (tokenResponse.getStatus() != 200) {
throw new HttpResponseException("URL: " + loginRequest.getURI() + ", Error: "
+ tokenResponse.getStatus() + ", Message: " + tokenResponse.getContentAsString(),
tokenResponse);
}
String authCode = getAuthCode(tokenResponse.getContentAsString());

/**
* Step 4) Decode access token
*/
ChinaTokenResponse cat = JsonStringDeserializer.deserializeString(authCode, ChinaTokenResponse.class);
String token = cat.data.accessToken;
// https://www.baeldung.com/java-jwt-token-decode
String[] chunks = token.split("\\.");
String tokenJwtDecodeStr = new String(Base64.getUrlDecoder().decode(chunks[1]));
ChinaTokenExpiration cte = JsonStringDeserializer.deserializeString(tokenJwtDecodeStr,
ChinaTokenExpiration.class);

AccessTokenResponse t = new AccessTokenResponse();
t.setAccessToken(token);
t.setTokenType(cat.data.tokenType);
t.setCreatedOn(tokenCreatedOn);
t.setExpiresIn(cte.exp);

this.tokenResponse = t;
return true;
} catch (Exception e) {
logger.warn("Authorization Exception: {}", e.getMessage());
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@
*
* @author Bernd Weymann - Initial contribution
* @author Martin Grassl - update to v2 API
* @author Mark Herwege - remove China
*/
@NonNullByDefault
public interface BimmerConstants {

static final String REGION_NORTH_AMERICA = "NORTH_AMERICA";
static final String REGION_CHINA = "CHINA";
static final String REGION_ROW = "ROW";

static final String BRAND_BMW = "bmw";
Expand All @@ -46,24 +46,19 @@ public interface BimmerConstants {

static final String EADRAX_SERVER_NORTH_AMERICA = "cocoapi.bmwgroup.us";
static final String EADRAX_SERVER_ROW = "cocoapi.bmwgroup.com";
static final String EADRAX_SERVER_CHINA = "myprofile.bmw.com.cn";
static final Map<String, String> EADRAX_SERVER_MAP = Map.of(REGION_NORTH_AMERICA, EADRAX_SERVER_NORTH_AMERICA,
REGION_CHINA, EADRAX_SERVER_CHINA, REGION_ROW, EADRAX_SERVER_ROW);
REGION_ROW, EADRAX_SERVER_ROW);

static final String OCP_APIM_KEY_NORTH_AMERICA = "31e102f5-6f7e-7ef3-9044-ddce63891362";
static final String OCP_APIM_KEY_ROW = "4f1c85a3-758f-a37d-bbb6-f8704494acfa";
static final Map<String, String> OCP_APIM_KEYS = Map.of(REGION_NORTH_AMERICA, OCP_APIM_KEY_NORTH_AMERICA,
REGION_ROW, OCP_APIM_KEY_ROW);

static final String CHINA_PUBLIC_KEY = "/eadrax-coas/v1/cop/publickey";
static final String CHINA_LOGIN = "/eadrax-coas/v2/login/pwd";

// Http variables
static final String APP_VERSION_NORTH_AMERICA = "4.9.2(36892)";
static final String APP_VERSION_ROW = "4.9.2(36892)";
static final String APP_VERSION_CHINA = "4.9.2(36892)";
static final Map<String, String> APP_VERSIONS = Map.of(REGION_NORTH_AMERICA, APP_VERSION_NORTH_AMERICA, REGION_ROW,
APP_VERSION_ROW, REGION_CHINA, APP_VERSION_CHINA);
APP_VERSION_ROW);
static final String USER_AGENT = "Dart/2.16 (dart:io)";
// see const.py of bimmer_constants: user-agent; brand; app_version; region
static final String X_USER_AGENT = "android(AP2A.240605.024);%s;%s;%s";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
<description>Select Region in order to connect to the appropriate BMW Server</description>
<options>
<option value="NORTH_AMERICA">North America</option>
<option value="CHINA">China</option>
<option value="ROW">Rest of the World</option>
</options>
<default>ROW</default>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ thing-type.config.mybmw.bridge.hcaptchatoken.description = Captcha-Token for the
thing-type.config.mybmw.bridge.hcaptchatoken.label = Captcha-Token
thing-type.config.mybmw.bridge.region.description = Select Region in order to connect to the appropriate BMW Server
thing-type.config.mybmw.bridge.region.label = Region
thing-type.config.mybmw.bridge.region.option.CHINA = China
thing-type.config.mybmw.bridge.region.option.NORTH_AMERICA = North America
thing-type.config.mybmw.bridge.region.option.ROW = Rest of the World
thing-type.config.mybmw.bridge.userName.description = MyBMW Username
Expand Down

0 comments on commit b0453e8

Please sign in to comment.