Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[kostal-inverter] Cannot connect to PLENTICORE PLUS 10.0 #7492

Closed
Bonifatius94 opened this issue Apr 27, 2020 · 20 comments
Closed

[kostal-inverter] Cannot connect to PLENTICORE PLUS 10.0 #7492

Bonifatius94 opened this issue Apr 27, 2020 · 20 comments
Labels
bug An unexpected problem or unintended behavior of an add-on

Comments

@Bonifatius94
Copy link

Bonifatius94 commented Apr 27, 2020

Hello everyone,

I've got an openHAB instance running as a docker container and want to connect to my Kostal inverter (model: PLENTICORE PLUS 10.0). The connection to other "things" in the LAN works, so I assume that it's not a network issue, but rather an authentication error. And I'm also sure that the password is correct as I can log into the scb website as well.

Have you already tested this code explicitly for my device? I've tried to debug into your code, but the last authentication step /auth/create_session always seems to fail. I'm getting 400 BadRequest. The previous steps /auth/start and /auth/finish seem to work fine.

Thanks for you help in advance!

Best wishes
Marco

Expected Behavior

openHAB should be able to establish connections to Kostal PLENTICORE PLUS 10.0 devices using the kostal-inverter binding

Current Behavior

As you can see from the event log, I've been installing a thing using the kostal-inverter binding. The thing first goes into INITIALIZING state, then into UNKNOWN state and after creating several links it finally goes into OFFLINE (COMMUNICATION_ERROR) state. The message also tells that there is an authentication error which support my former suggestions when debugging the code. Althouh I had to restart the container because it showed me 503 No connection error.

event log:
2020-04-27 22:29:28.971 [thome.event.ExtensionEvent] - Extension 'package-standard' has been installed.
2020-04-27 22:29:47.414 [thome.event.ExtensionEvent] - Extension 'ui-homebuilder' has been installed.
2020-04-27 22:29:47.415 [thome.event.ExtensionEvent] - Extension 'ui-basic' has been installed.
2020-04-27 22:29:47.415 [thome.event.ExtensionEvent] - Extension 'ui-habpanel' has been installed.
2020-04-27 22:29:47.415 [thome.event.ExtensionEvent] - Extension 'ui-paper' has been installed.
2020-04-27 22:30:59.166 [thome.event.ExtensionEvent] - Extension 'binding-kostalinverter' has been installed.
2020-04-27 22:44:56.946 [hingStatusInfoChangedEvent] - 'kostalinverter:PLENTICOREPLUS100WITHBATTERY:06b8746e' changed from UNINITIALIZED to INITIALIZING
2020-04-27 22:44:56.955 [hingStatusInfoChangedEvent] - 'kostalinverter:PLENTICOREPLUS100WITHBATTERY:06b8746e' changed from INITIALIZING to UNKNOWN
2020-04-27 22:44:58.164 [hingStatusInfoChangedEvent] - 'kostalinverter:PLENTICOREPLUS100WITHBATTERY:06b8746e' changed from UNKNOWN to OFFLINE (COMMUNICATION_ERROR): Error during the initialisation of the authentication
2020-04-27 22:45:07.082 [hingStatusInfoChangedEvent] - 'kostalinverter:PLENTICOREPLUS100WITHBATTERY:06b8746e' changed from OFFLINE (COMMUNICATION_ERROR): Error during the initialisation of the authentication to OFFLINE (COMMUNICATION_ERROR): HTTP communication error: No response from device

Possible Solution

  1. Reproduce error with a build of the current source code
  2. Try to make the authentication work with a Java implementation
  3. Determine differences between both implementations (which hopefully leads to a possible fix)
  4. Implement the fix and review it

Steps to Reproduce (for Bugs)

  1. Have a Kostal inverter PLENTICORE PLUS 10.0 plugged to your LAN
  2. Find out the IP of the inverter and make sure it can be pinged
  3. Install an openHAB server using docker-compose with standard composition from docs (I'm using Ubuntu 18.04 as host OS)
version: '2.2'

services:
  openhab:
    image: "openhab/openhab:2.5.4"
    restart: always
    network_mode: host
    volumes:
      - "/etc/localtime:/etc/localtime:ro"
      - "/etc/timezone:/etc/timezone:ro"
      - "./openhab_addons:/openhab/addons"
      - "./openhab_conf:/openhab/conf"
      - "./openhab_userdata:/openhab/userdata"
    environment:
      OPENHAB_HTTP_PORT: "8080"
      OPENHAB_HTTPS_PORT: "8443"
      EXTRA_JAVA_OPTS: "-Duser.timezone=Europe/Berlin"
  1. (Wait about 10-20 seconds) -> open http://localhost:8080 into your webbrowser
  2. Choose Standard Setup -> (Wait for completion, THIS IS IMPORTANT!!!)
  3. Choose PaperUI
  4. Addons -> Bindings -> Kostal Inverter Binding -> install -> (Wait for completion)
  5. Configuration -> Things -> + -> Kostal Inverter Binding -> Kostal PLENTICORE PLUS 10.0 (with Battery)
  6. put IP address and master key, keep refresh interval at 30sec -> confirm

Context

I just want to retrieve sensor data from my Kostal inverter and combine it with weather data to send notifications when to e.g. switch on the wasching machine etc. to increase my self-sufficiency (and hopefully save money). This is just a freetime project, so nothing critical.

Your Environment

  • Version used: 2.5.4 (both openHAB and kostal-inverter binding)
  • Environment name and version (e.g. Chrome 76, Java 8, Node.js 12.9, ...): Firefox browser (75.0 64-Bit)
  • Operating System and version (desktop or mobile, Windows 10, Raspbian Buster, ...): official docker image with Ubuntu 18.04 as host machine
@Bonifatius94 Bonifatius94 added the bug An unexpected problem or unintended behavior of an add-on label Apr 27, 2020
@Bonifatius94
Copy link
Author

I could actually connect to my kostal inverter using this python code:
https://stackoverflow.com/questions/59053539/api-call-portation-from-java-to-python-kostal-plenticore-inverter

Now, I'm comparing the two codes and figuring out what's the issue with the Java implementation.

@Bonifatius94
Copy link
Author

I made the authentication code work with following code. It does not seem too different from the original code. so I'm wondering whether this code is already shipped with the latest docker image.

package authtest;

import com.google.gson.*;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpVersion;

import javax.crypto.*;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class OriginalAuthTest {

    public static void main(String[] args) {

        OriginalAuthTest test = new OriginalAuthTest();

        final String host = "192.168.178.149";
        final String userPassword = "xxxxxxxx";

        test.initHttpClient();
        test.authenticate(host, userPassword);
        test.closeHttpClient();
    }

    private HttpClient httpClient;

    public void initHttpClient() {

        httpClient = new HttpClient();
        httpClient.setFollowRedirects(false);

        try {
            httpClient.start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void closeHttpClient() {

        try {
            httpClient.stop();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // ===================================================================================================

    /*
     * operations used for authentication
     */
    private static final String AUTH_START = "/auth/start";
    private static final String AUTH_FINISH = "/auth/finish";
    private static final String AUTH_CREATE_SESSION = "/auth/create_session";

    /*
     * operations used for gathering process data from the device
     */
    private static final String PROCESSDATA = "/processdata";

    /**
     * This function is used to authenticate against the SCB.
     * SCB uses PBKDF2 and AES256 GCM mode with a slightly modified authentication message.
     * The authentication will fail on JRE < 8u162. since the security policy is set to "limited" by default (see readme
     * for fix)
     */
    public void authenticate(String host, String userPassword) {

        // Create random numbers
        String clientNonce = createClientNonce();

        // Perform first step of authentication
        JsonObject authMeJsonObject = new JsonObject();
        authMeJsonObject.addProperty("username", USER_TYPE);
        authMeJsonObject.addProperty("nonce", clientNonce);

        ContentResponse authStartResponseContentResponse;
        try {
            authStartResponseContentResponse = executeHttpPost(httpClient, host, AUTH_START, authMeJsonObject);

            // 200 is the desired status code
            int statusCode = authStartResponseContentResponse.getStatus();

            if (statusCode == 400) {
                // Invalid user (which is hard coded and therefore can not be wrong until the api is changed by the
                // manufacturer
                return;
            }
            if (statusCode == 403) {
                // User is logged
                // This can happen, if the user had to many bad attempts of entering the password in the web
                // front end
                return;
            }

            if (statusCode == 503) {
                // internal communication error
                // This can happen if the device is not ready yet for communication
                return;
            }
        } catch (InterruptedException | TimeoutException | ExecutionException e) {
            return;
        }
        JsonObject authMeResponseJsonObject = getJsonObjectFromResponse(authStartResponseContentResponse);

        // Extract information from the response
        String salt = authMeResponseJsonObject.get("salt").getAsString();
        String serverNonce = authMeResponseJsonObject.get("nonce").getAsString();
        int rounds = authMeResponseJsonObject.get("rounds").getAsInt();
        String transactionId = authMeResponseJsonObject.get("transactionId").getAsString();

        // Do the cryptography stuff (magic happens here)
        byte[] saltedPasswort;
        byte[] clientKey;
        byte[] serverKey;
        byte[] storedKey;
        byte[] clientSignature;
        byte[] serverSignature;
        String authMessage;
        try {
            saltedPasswort = getPBKDF2Hash(userPassword, Base64.getDecoder().decode(salt), rounds);
            clientKey = getHMACSha256(saltedPasswort, "Client Key");
            serverKey = getHMACSha256(saltedPasswort, "Server Key");
            storedKey = getSha256Hash(clientKey);
            authMessage = String.format("n=%s,r=%s,r=%s,s=%s,i=%d,c=biws,r=%s", USER_TYPE, clientNonce, serverNonce,
                    salt, rounds, serverNonce);
            clientSignature = getHMACSha256(storedKey, authMessage);
            serverSignature = getHMACSha256(serverKey, authMessage);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException | IllegalStateException e2) {
            return;
        }
        String clientProof = createClientProof(clientSignature, clientKey);
        // Perform step 2 of the authentication
        JsonObject authFinishJsonObject = new JsonObject();
        authFinishJsonObject.addProperty("transactionId", transactionId);
        authFinishJsonObject.addProperty("proof", clientProof);

        ContentResponse authFinishResponseContentResponse;
        JsonObject authFinishResponseJsonObject;
        try {
            authFinishResponseContentResponse = executeHttpPost(httpClient, host, AUTH_FINISH, authFinishJsonObject);
            authFinishResponseJsonObject = getJsonObjectFromResponse(authFinishResponseContentResponse);
            // 200 is the desired status code
            if (authFinishResponseContentResponse.getStatus() == 400) {
                // Authentication failed
                return;
            }
        } catch (InterruptedException | TimeoutException | ExecutionException e3) {
            return;
        }

        // Extract information from the response
        byte[] signature = Base64.getDecoder().decode(authFinishResponseJsonObject.get("signature").getAsString());
        String token = authFinishResponseJsonObject.get("token").getAsString();

        // Validate provided signature against calculated signature
        if (!java.util.Arrays.equals(serverSignature, signature)) {
            return;
        }

        // Calculate protocol key
        SecretKeySpec signingKey = new SecretKeySpec(storedKey, HMAC_SHA256_ALGORITHM);
        Mac mac;
        byte[] protocolKeyHMAC;
        try {
            mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
            mac.init(signingKey);
            mac.update("Session Key".getBytes());
            mac.update(authMessage.getBytes());
            mac.update(clientKey);
            protocolKeyHMAC = mac.doFinal();
        } catch (NoSuchAlgorithmException | InvalidKeyException e1) {
            // Since the necessary libraries are provided, this should not happen
            return;
        }

        byte[] data;
        byte[] iv;

        // AES GCM stuff
        iv = new byte[16];

        new SecureRandom().nextBytes(iv);

        SecretKeySpec skeySpec = new SecretKeySpec(protocolKeyHMAC, "AES");
        GCMParameterSpec param = new GCMParameterSpec(protocolKeyHMAC.length * 8 - AES_GCM_TAG_LENGTH, iv);

        Cipher cipher;
        try {
            cipher = Cipher.getInstance("AES_256/GCM/NOPADDING");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, param);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e1) {
            // The java installation does not support AES encryption in GCM mode
            return;
        }
        try {
            data = cipher.doFinal(token.getBytes("UTF-8"));
        } catch (IllegalBlockSizeException | BadPaddingException | UnsupportedEncodingException e1) {
            // No JSON answer received
            return;
        }

        byte[] ciphertext = new byte[data.length - AES_GCM_TAG_LENGTH / 8];
        byte[] gcmTag = new byte[AES_GCM_TAG_LENGTH / 8];
        System.arraycopy(data, 0, ciphertext, 0, data.length - AES_GCM_TAG_LENGTH / 8);
        System.arraycopy(data, data.length - AES_GCM_TAG_LENGTH / 8, gcmTag, 0, AES_GCM_TAG_LENGTH / 8);

        JsonObject createSessionJsonObject = new JsonObject();
        createSessionJsonObject.addProperty("transactionId", transactionId);
        createSessionJsonObject.addProperty("iv", Base64.getEncoder().encodeToString(iv));
        createSessionJsonObject.addProperty("tag", Base64.getEncoder().encodeToString(gcmTag));
        createSessionJsonObject.addProperty("payload", Base64.getEncoder().encodeToString(ciphertext));

        // finally create the session for further communication
        ContentResponse createSessionResponseContentResponse;
        JsonObject createSessionResponseJsonObject;
        try {
            createSessionResponseContentResponse = executeHttpPost(httpClient, host, AUTH_CREATE_SESSION, createSessionJsonObject);
            createSessionResponseJsonObject = getJsonObjectFromResponse(createSessionResponseContentResponse);
        } catch (InterruptedException | TimeoutException | ExecutionException e) {
            return;
        }
        // 200 is the desired status code
        if (createSessionResponseContentResponse.getStatus() == 400) {
            return;
        }

        String sessionId = createSessionResponseJsonObject.get("sessionId").getAsString();
        System.out.println("sessionId: " + sessionId);
    }

    // ===================================================================================================

    // List of all constants used for the authentication
    static final String USER_TYPE = "user";
    static final String HMAC_SHA256_ALGORITHM = "HMACSHA256";
    static final String SHA_256_HASH = "SHA-256";
    static final int AES_GCM_TAG_LENGTH = 128; // bit count

    // ===================================================================================================

    /**
     * This method generates the HMACSha256 encrypted value of the given value
     *
     * @param password Password used for encryption
     * @param valueToEncrypt value to encrypt
     * @return encrypted value
     * @throws InvalidKeyException thrown if the key generated from the password is invalid
     * @throws NoSuchAlgorithmException thrown if HMAC SHA 256 is not supported
     */
    static byte[] getHMACSha256(byte[] password, String valueToEncrypt)
            throws InvalidKeyException, NoSuchAlgorithmException {
        SecretKeySpec signingKey = new SecretKeySpec(password, HMAC_SHA256_ALGORITHM);
        Mac mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
        mac.init(signingKey);
        mac.update(valueToEncrypt.getBytes());
        return mac.doFinal();
    }

    /**
     * This methods generates the client proof.
     * It is calculated as XOR between the {@link clientSignature} and the {@link serverSignature}
     *
     * @param clientSignature client signature
     * @param serverSignature server signature
     * @return client proof
     */
    static String createClientProof(byte[] clientSignature, byte[] serverSignature) {
        byte[] result = new byte[clientSignature.length];
        for (int i = 0; i < clientSignature.length; i++) {
            result[i] = (byte) (0xff & (clientSignature[i] ^ serverSignature[i]));
        }
        return Base64.getEncoder().encodeToString(result);
    }

    /**
     * Create the PBKDF2 hash
     *
     * @param password password
     * @param salt salt
     * @param rounds rounds
     * @return hash
     * @throws NoSuchAlgorithmException if PBKDF2WithHmacSHA256 is not supported
     * @throws InvalidKeySpecException if the key specification is not supported
     */
    static byte[] getPBKDF2Hash(String password, byte[] salt, int rounds)
            throws NoSuchAlgorithmException, InvalidKeySpecException {

        PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt, rounds, 256);
        SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");

        return skf.generateSecret(spec).getEncoded();
    }

    /**
     * Create the SHA256 hash value for the given byte array
     *
     * @param valueToHash byte array to get the hash value for
     * @return the hash value
     * @throws NoSuchAlgorithmException if SHA256 is not supported
     */
    static byte[] getSha256Hash(byte[] valueToHash) throws NoSuchAlgorithmException {
        return MessageDigest.getInstance(SHA_256_HASH).digest(valueToHash);
    }

    /**
     * Create the nonce (numbers used once) for the client for communication
     *
     * @return nonce
     */
    static String createClientNonce() {

        Random generator = new Random();

        // Randomize the random generator
        byte[] randomizeArray = new byte[1024];
        generator.nextBytes(randomizeArray);

        // 3 words of 4 bytes are required for the handshake
        byte[] nonceArray = new byte[12];
        generator.nextBytes(nonceArray);

        // return the base64 encoded value of the random words
        return Base64.getMimeEncoder().encodeToString(nonceArray);
    }

    // ===================================================================================================

    // base URL of the web api
    private static final String WEB_API = "/api/v1";
    // GSON handler
    private static final Gson GSON = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
            .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();

    /**
     * Helper function to execute a HTTP post request
     *
     * @param httpClient httpClient to use for communication
     * @param url IP or hostname or the device
     * @param resource web API resource to post to
     * @param parameters the JSON content to post
     * @return the HTTP response for the created post request
     * @throws ExecutionException Error during the execution of the http request
     * @throws TimeoutException Connection timed out
     * @throws InterruptedException Connection interrupted
     */
    static ContentResponse executeHttpPost(HttpClient httpClient, String url, String resource, JsonObject parameters)
            throws InterruptedException, TimeoutException, ExecutionException {
        return executeHttpPost(httpClient, url, resource, parameters, null);
    }

    /**
     * Helper function to execute a HTTP post request
     *
     * @param httpClient httpClient to use for communication
     * @param url IP or hostname or the device
     * @param resource web API resource to post to
     * @param sessionId optional session ID
     * @param parameters the JSON content to post
     * @return the HTTP response for the created post request
     * @throws ExecutionException Error during the execution of the http request
     * @throws TimeoutException Connection timed out
     * @throws InterruptedException Connection interrupted
     */
    static ContentResponse executeHttpPost(HttpClient httpClient, String url, String resource, JsonElement parameters, @Nullable String sessionId)
            throws InterruptedException, TimeoutException, ExecutionException {

        Request response = httpClient.newRequest(String.format("%s/%s%s", url, WEB_API, resource), 80).scheme("http")
                .agent("Jetty HTTP client").version(HttpVersion.HTTP_1_1).method(HttpMethod.POST)
                .header(HttpHeader.ACCEPT, "application/json").header(HttpHeader.CONTENT_TYPE, "application/json")
                .timeout(5, TimeUnit.SECONDS);

        response.content(new StringContentProvider(parameters.toString()));

        if (sessionId != null) {
            response.header(HttpHeader.AUTHORIZATION, String.format("Session %s", sessionId));
        }

        return response.send();
    }

    /**
     * Helper function to execute a HTTP get request
     *
     * @param httpClient httpClient to use for communication
     * @param url IP or hostname or the device
     * @param resource web API resource to get
     * @return the HTTP response for the created get request
     * @throws ExecutionException Error during the execution of the http request
     * @throws TimeoutException Connection timed out
     * @throws InterruptedException Connection interrupted
     */
    static ContentResponse executeHttpGet(HttpClient httpClient, String url, String resource)
            throws InterruptedException, TimeoutException, ExecutionException {
        return executeHttpGet(httpClient, url, resource, null);
    }

    /**
     * Helper function to execute a HTTP get request
     *
     * @param httpClient httpClient to use for communication
     * @param url IP or hostname or the device
     * @param resource web API resource to get
     * @param sessionId optional session ID
     * @return the HTTP response for the created get request
     * @throws ExecutionException Error during the execution of the http request
     * @throws TimeoutException Connection timed out
     * @throws InterruptedException Connection interrupted
     * @throws Exception thrown if there are communication problems
     */
    static ContentResponse executeHttpGet(HttpClient httpClient, String url, String resource, @Nullable String sessionId)
            throws InterruptedException, TimeoutException, ExecutionException {

        Request response = httpClient.newRequest(String.format("%s/%s%s", url, WEB_API, resource), 80).scheme("http")
                .agent("Jetty HTTP client").version(HttpVersion.HTTP_1_1).method(HttpMethod.GET)
                .header(HttpHeader.ACCEPT, "application/json").header(HttpHeader.CONTENT_TYPE, "application/json")
                .timeout(5, TimeUnit.SECONDS);

        if (sessionId != null) {
            response.header(HttpHeader.AUTHORIZATION, String.format("Session %s", sessionId));
        }

        return response.send();
    }

    /**
     * Helper to extract the JsonArray from a HTTP response.
     * Use only, if you expect a JsonArray and no other types (e.g. JSON array)!
     *
     * @param reponse the HTTP response
     * @return the JSON object
     */
    static JsonArray getJsonArrayFromResponse(ContentResponse reponse) {
        return GSON.fromJson(reponse.getContentAsString(), JsonArray.class);
    }

    /**
     * Helper to extract the JSON object from a HTTP response.
     * Use only, if you expect a JSON object and no other types (e.g. JSON array)!
     *
     * @param reponse the HTTP response
     * @return the JSON object
     */
    static JsonObject getJsonObjectFromResponse(ContentResponse reponse) {
        return GSON.fromJson(reponse.getContentAsString(), JsonObject.class);
    }
}

My packages I used as pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>kostalinverter</groupId>
    <artifactId>kostalinverter-authtest</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-client</artifactId>
            <version>9.4.28.v20200408</version>
        </dependency>

        <dependency>
            <groupId>org.eclipse.jdt</groupId>
            <artifactId>org.eclipse.jdt.annotation</artifactId>
            <version>2.2.400</version>
        </dependency>

        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.6</version>
        </dependency>

        <dependency>
            <groupId>com.google.code.findbugs</groupId>
            <artifactId>jsr305</artifactId>
            <version>2.0.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

I'm using Java version 1.8.0_172. Maybe the Java version makes the code fail.

@pfoerti10
Copy link

Hi,
do I understand it correctly that you´re now able to connect to the inverter via the binding?
I´m currently stuck at the same issue you mentioned in your initial post.

BR Stefan

@Bonifatius94
Copy link
Author

Hi Stefan,

unfortunately I could only make my code snippet work (see post). So, at least we've got a fallback for the authentication in Java that works fine.

The next step would be making a nightly build and testing it as is. And if the issue still remains I would go further into debugging the code.

Do you know how to make a nightly build? I couldn't figure it out yet.

Best wishes
Marco

@pfoerti10
Copy link

Hi Marco,

I´m sorry but I don´t know how to do nightly builds.
I´m neither good in Java (has been some years since I used it), so I can inly scratch on the surface of the problem.
Where can I find out which Java-version I´m using? So I could tell you if it is the same as you use.

I recognized that some steps of the authentication seem to work as a wrong password is detected immediately.

BR Stefan

@Bonifatius94
Copy link
Author

Hi Stefan,

you can be prompted the java version by executing following command:

java -version

But I actually don't think that the Java version is the matter. It's more about the openHAB release 2.5.4.

How are you running openHAB? Do you use a docker setup or a plain installation? In case of a docker setup you need to attach to the interactive docker console, first. This can be achieved by following command:

docker exec -it <container-name> bash

Best wishes
Marco

@Bonifatius94
Copy link
Author

In the meantime I was able to download the addon-repo and successfully build the current source code (binary=jar-file). I could also manage to apply the jar-file to my local openHAB instance (this is possible by simply putting the jar-file into the addons folder of your openHAB installation and restart openHAB for loading it). And what can I say ... unfortunately the error showed up again, so now we can at least be sure that the current source code is broken and needs to be fixed.

In my next step, I'm going to debug into the addon-code (see openHAB documentation: https://www.openhab.org/docs/developer/ide/intellij.html). Maybe I can find out the issue that way.

@Bonifatius94
Copy link
Author

I added some lines to log some useful debugging information. The failure acutually occurs at step 3 /auth/create_session of the authentication process (as suggested in my initial post). It fails at lines 432/433 of ThirdGenerationHandler.java with "java.security.InvalidKeyException: Illegal key size" exception.

signature: /oWmrNWKJ7h6xf8KM1kX0fTCz8GCbtPkJGKkO94qbIw=
protocol key: FXWbBd8nacTFEDkrjTWm7mOmhCi4uDU8+CR7OJflIB4=

Does anyone know why there is an illegal key size? And why does the same code actually work fine with other java versions (e.g. java 8 update 172)?

@pfoerti10
Copy link

Hi,
in the meanwhile I checked my Java version, it´s 1.8.0_152.
I´m running a plain 2.5.4-1 installation on a raspberry;

Before that I tried the same with a 2.4 installation, where the thing-configuration in paper UI looks a little bit different (+ username).
There I got a error message regarding an wrong index.

So far unfortunately no other news from me ;)

@Bonifatius94
Copy link
Author

Hi Stefan,

there is a known issue with Java versions lower than 1.8_u162 because Java developers changed their encryption policy in terms of key strength, so your openHAB instance will probably run into an invalid key length exception. Unfortunately the fix for this issue is done by manually changing a configuration file in your Java runtime. And by doing so, you would change the security policy for your whole openHAB instance (or even your entire raspi) which may obviously by critical to other applications.

What makes me curious is that I'm also encountering issues in that regard, even though the Java version inside my docker container is 1.8_u232 (openHAB image 2.5.4) which should be absolutely fine as versions above 1.8_u162 have the stronger crypto policy setting automatically activated.

I found out that it is possible to manipulate the crypto policy programmatically. But then you need to execute this line at application startup before the crypto tools are used for the first time.

Security.setProperty("crypto.policy", "unlimited");

In my opinion there should be a programatic solution which does not affect other parts of openHAB. I only see two alternatives to solving this issue:

  1. run authentication code in a separate Java runtime with manipulated crypto settings (rather quick fix, dirty but should work 100%)
  2. implement crypto functions with another crypto API that lets you specify the security settings programmatically
  3. (change the crypto settings file inside your Java runtime -> unsatisfying solution as pointed out above)

@Bertl519
Copy link

Bertl519 commented May 4, 2020

Hi Marco,

I am writing because of your invitation in the according thread in the OpenHab community :-) First of all i want to thank you very much for your effort in this topic!

Unfortunately i can not support you very much, my coding skills are very limited...

I have checked my Java version, I am running 1.8.0_152 too. What do we have to do for solving this issue according your first alternative?
Do i understand right, it is a general problem with the binding? Because all standard OH installations are running with Java version 1.8.0_152, or not?

BR
Bertl

@Bonifatius94
Copy link
Author

Hi Bertl,

not everyone is using the same Java version, e.g. I'm using the official docker image of openHAB (2.5.4) which is running Java version 1.8_u232 inside the container.

Simply said, the issue is that the authentication fails due to the change of encryption strength made by the Java devs. And it's not really binding-related because the source code is fine. The execution environment (Java runtime) is the problem as it provides security settings that make the authentication fail. So, it's more of a configuration / hosting issue.

Obviously you could try to install a Java version > 1.8_u161 which activates the stronger encryption by default that is required for your kostal inverter. Or alternatively you could keep your Java version and only make changes to the security settings file. But this may lead to further issues with other Java applications that are running onto the same machine. So, it really depends on your particular setup and if you are willing to take the risk of other applications to fail due to those changes.

I hope this post sums the problem a bit up and shows quick fixes.

Best wishes
Marco

@Bonifatius94
Copy link
Author

Hello everyone,

I just managed to fix the kostal-inverter binding's authentication issue. You just need to change the $JAVA_HOME/jre/lib/security/java.security file. There is a line crypto.policy=limited which needs to be changed to crypto.policy=unlimited. (you need to scroll quite a bit down to find it) After restarting the service, the authentication worked fine. But as I pointed out before there may be collateral damage doing so as other Java applications may require the limited setting ...

I'm just wondering why the official openHAB docker image has this setting set to limited even though all java 8 versions greater than 161 have the unlimited setting by default. This means they changed it back to limited on purpose. I don't know whether there is another component of openHAB that requires the value to be set to limited. Maybe I should ask the docker image team why.

Anyways ... thank you for your participation in this issue.

Best wishes
Marco

@Bonifatius94
Copy link
Author

I just did a little research on the 2.5.4 openHAB docker image that may help enabling the unlimited security setting. The entrypoint.sh file (see https://github.com/openhab/openhab-docker/blob/master/2.5.4/debian/entrypoint.sh) checks for an environment variable CRYPTO_POLICY. If this variable is set to 'unlimited' then the Java security policy is enabled automatically.

version: '2.2'

services:
  openhab:
    image: "openhab/openhab:2.5.4"
    restart: always
    network_mode: host
    volumes:
      - "/etc/localtime:/etc/localtime:ro"
      - "/etc/timezone:/etc/timezone:ro"
      - "./openhab_addons:/openhab/addons"
      - "./openhab_conf:/openhab/conf"
      - "./openhab_userdata:/openhab/userdata"
    environment:
      OPENHAB_HTTP_PORT: "8080"
      OPENHAB_HTTPS_PORT: "8443"
      EXTRA_JAVA_OPTS: "-Duser.timezone=Europe/Berlin"
      CRYPTO_POLICY: "unlimited"

@Bonifatius94
Copy link
Author

Just read the README page of the official docker image. They actually don't enable the unlimited security settings due to legal law issues as stronger encryption might not be used in some countries. Are you f*ckin kiddin me??? This cost me 2 weeks of my life.

https://github.com/openhab/openhab-docker#java-cryptographic-strength-policy

@pfoerti10
Copy link

Hi Bonifatius,

at first thank you very much for diggin so deep into this topic!
I´m sorry I didn´t respond these days as job and family keeping me busy ;)

I tried to follow your instructions and found the line,
and after modifying it I finally (!) have a connection to the plenticore.

Currently I´m checking if any of my other services are impacted...

Thank you very much for your effort and help!!!

BR Stefan

@Bonifatius94
Copy link
Author

You're welcome, Stefan.

I've proposed a merge request to the kostal inverter documentation, so people see at first glance that they need to turn on strong crypto settings to connect to their kostal inverter.

@Bertl519
Copy link

Hi Marco,

I have also tried your suggested changes and now the binding is working! Up to now i could not find out any problems with other services…

Thank you very much for taking care about this problem! I hope your proposal will be realized soon, for people with poor coding knowledge it`s nearly impossible to solve this problem…

BR
Bertl

@Bonifatius94
Copy link
Author

Hi Bertl,

my changes to the official addon documentation are already merged into the source code of the next major version 2.5.5 (see #7581). Though it was quite hard to propose those changes only affecting 4 lines of the README.md file. Oh well ...

The activation of the strong cryptography should not harm your system at all. It just prevents an exception to be thrown when you try to use strong cryptography without the licence permission bound to your country, so e.g. terrorists might not use strong encrypted ciphers. So it does not change the behavior of weak encryption.

Thank you very much for your participation. You brought me to the right suggestions.

Best wishes
Marco

@openhab-bot
Copy link
Collaborator

This issue has been mentioned on openHAB Community. There might be relevant details there:

https://community.openhab.org/t/kostal-inverter-plenticore-plus-10-fw-01-42-no-connection/97322/43

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug An unexpected problem or unintended behavior of an add-on
Projects
None yet
Development

No branches or pull requests

4 participants