mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[icloud] Implement SRP authentication algorithm (#19832)
Signed-off-by: Simon Spielmann <simon.spielmann@gmx.de>
This commit is contained in:
@@ -15,4 +15,21 @@
|
||||
|
||||
<description>The Apple iCloud is used to retrieve data such as the battery level or current location of one or
|
||||
multiple Apple devices connected to an iCloud account.</description>
|
||||
|
||||
<properties>
|
||||
<bouncycastle.version>1.81</bouncycastle.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcprov-jdk18on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.bouncycastle</groupId>
|
||||
<artifactId>bcutil-jdk18on</artifactId>
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
*
|
||||
* Exception for errors during calls of the iCloud API.
|
||||
*
|
||||
* @author Simon Spielmann - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ICloudApiAuthenticationException extends ICloudApiResponseException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param url URL for which the exception occurred
|
||||
* @param statusCode HTTP status code which was reported
|
||||
*/
|
||||
public ICloudApiAuthenticationException(String url, int statusCode, String body) {
|
||||
super(url, statusCode, body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ICloudApiAuthenticationException [statusCode=" + statusCode + ", body=" + body + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the reason for the authentication error based on the status code.
|
||||
*
|
||||
* @return statusCode HTTP status code of failed request.
|
||||
*/
|
||||
public String getReason() {
|
||||
switch (this.statusCode) {
|
||||
case 421:
|
||||
return "LOGIN_TOKEN_EXPIRED";
|
||||
case 409:
|
||||
return "2FA_REQUIRED";
|
||||
case 450:
|
||||
return "FIND_MY_AUTH_REQUIRED";
|
||||
case 500:
|
||||
return "GENERAL_AUTH_ERROR";
|
||||
default:
|
||||
return "UNKNOWN_ERROR";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given status code is related to an authentication error.
|
||||
*
|
||||
* @param statusCode HTTP status code to check.
|
||||
* @return true if the status code indicates an authentication error, false otherwise.
|
||||
*/
|
||||
public static boolean isAuthError(int statusCode) {
|
||||
return statusCode == 409 || statusCode == 421 || statusCode == 450 || statusCode == 500;
|
||||
}
|
||||
}
|
||||
+9
-2
@@ -24,7 +24,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
public class ICloudApiResponseException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private int statusCode;
|
||||
protected int statusCode;
|
||||
protected String body;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
@@ -32,9 +33,15 @@ public class ICloudApiResponseException extends Exception {
|
||||
* @param url URL for which the exception occurred
|
||||
* @param statusCode HTTP status code which was reported
|
||||
*/
|
||||
public ICloudApiResponseException(String url, int statusCode) {
|
||||
public ICloudApiResponseException(String url, int statusCode, String body) {
|
||||
super(String.format("Request %s failed with %s.", url, statusCode));
|
||||
this.statusCode = statusCode;
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ICloudApiResponseException [statusCode=" + statusCode + ", body=" + body + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+35
-25
@@ -13,12 +13,14 @@
|
||||
package org.openhab.binding.icloud.internal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.bouncycastle.crypto.CryptoException;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.icloud.internal.utilities.JsonUtils;
|
||||
@@ -28,6 +30,8 @@ import org.openhab.core.storage.Storage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
*
|
||||
* Class to access Apple iCloud API.
|
||||
@@ -39,9 +43,6 @@ import org.slf4j.LoggerFactory;
|
||||
@NonNullByDefault
|
||||
public class ICloudService {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final String ICLOUD_CLIENT_ID = "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ICloudService.class);
|
||||
@@ -84,10 +85,15 @@ public class ICloudService {
|
||||
*
|
||||
* @param forceRefresh Force a new authentication
|
||||
* @return {@code true} if authentication was successful
|
||||
*
|
||||
* @throws IOException if I/O error occurred
|
||||
* @throws InterruptedException if request was interrupted
|
||||
* @throws CryptoException if a cryptographic error occurred
|
||||
* @throws ICloudApiResponseException if the request failed (e.g. not OK HTTP return code)
|
||||
* @throws NoSuchAlgorithmException if the requested cryptographic algorithm is not available
|
||||
*/
|
||||
public boolean authenticate(boolean forceRefresh) throws IOException, InterruptedException {
|
||||
public boolean authenticate(boolean forceRefresh) throws IOException, InterruptedException,
|
||||
ICloudApiResponseException, CryptoException, NoSuchAlgorithmException {
|
||||
boolean loginSuccessful = false;
|
||||
if (this.session.getSessionToken() != null && !forceRefresh) {
|
||||
try {
|
||||
@@ -101,29 +107,33 @@ public class ICloudService {
|
||||
|
||||
if (!loginSuccessful) {
|
||||
logger.debug("Authenticating as {}...", this.appleId);
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("accountName", this.appleId);
|
||||
requestBody.put("password", this.password);
|
||||
requestBody.put("rememberMe", true);
|
||||
if (session.hasToken()) {
|
||||
requestBody.put("trustTokens", new String[] { this.session.getTrustToken() });
|
||||
} else {
|
||||
requestBody.put("trustTokens", new String[0]);
|
||||
}
|
||||
|
||||
List<Pair<String, String>> headers = getAuthHeaders();
|
||||
|
||||
try {
|
||||
this.session.post(AUTH_ENDPOINT + "/signin?isRememberMeEnabled=true", JsonUtils.toJson(requestBody),
|
||||
headers);
|
||||
} catch (ICloudApiResponseException ex) {
|
||||
return false;
|
||||
SrpAuthentication auth = new SrpAuthentication(appleId, password, getAuthHeaders());
|
||||
auth.auth(AUTH_ENDPOINT, session);
|
||||
} catch (ICloudApiAuthenticationException ex) {
|
||||
if (ex.getStatusCode() == 500) {
|
||||
logger.debug("Authentication failed.", ex);
|
||||
return false;
|
||||
} else {
|
||||
getMfaAuthOptions();
|
||||
}
|
||||
}
|
||||
}
|
||||
return authenticateWithToken();
|
||||
}
|
||||
|
||||
private void getMfaAuthOptions()
|
||||
throws JsonSyntaxException, IOException, InterruptedException, ICloudApiResponseException {
|
||||
List<Pair<String, String>> headers = ListUtil.replaceEntries(getAuthHeaders(),
|
||||
List.of(Pair.of("Accept", "application/json")));
|
||||
addSessionHeaders(headers);
|
||||
@Nullable
|
||||
Map<String, Object> localSessionData = JsonUtils.toMap(session.get(AUTH_ENDPOINT, headers));
|
||||
if (localSessionData != null) {
|
||||
data = localSessionData;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try authentication with stored session token. Returns {@code true} if authentication was successful.
|
||||
*
|
||||
@@ -172,12 +182,12 @@ public class ICloudService {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pair
|
||||
* @return
|
||||
*
|
||||
* @return List of headers required for authentication requests.
|
||||
*/
|
||||
private List<Pair<String, String>> getAuthHeaders() {
|
||||
return new ArrayList<>(List.of(Pair.of("Accept", "*/*"), Pair.of("Content-Type", "application/json"),
|
||||
Pair.of("X-Apple-OAuth-Client-Id", ICLOUD_CLIENT_ID),
|
||||
return new ArrayList<>(List.of(Pair.of("Accept", "application/json, text/javascript"),
|
||||
Pair.of("Content-Type", "application/json"), Pair.of("X-Apple-OAuth-Client-Id", ICLOUD_CLIENT_ID),
|
||||
Pair.of("X-Apple-OAuth-Client-Type", "firstPartyAuth"),
|
||||
Pair.of("X-Apple-OAuth-Redirect-URI", HOME_ENDPOINT),
|
||||
Pair.of("X-Apple-OAuth-Require-Grant-Code", "true"),
|
||||
|
||||
+6
-4
@@ -144,10 +144,6 @@ public class ICloudSession {
|
||||
logger.trace("Result {} {}\nHeaders -----\n{}\nBody -----\n{}\n------\n", url, response.statusCode(),
|
||||
response.headers(), responseBodyAsString);
|
||||
|
||||
if (response.statusCode() >= 300) {
|
||||
throw new ICloudApiResponseException(url, response.statusCode());
|
||||
}
|
||||
|
||||
// Store headers to reuse authentication
|
||||
this.data.accountCountry = response.headers().firstValue("X-Apple-ID-Account-Country")
|
||||
.orElse(getAccountCountry());
|
||||
@@ -158,6 +154,12 @@ public class ICloudSession {
|
||||
|
||||
this.stateStorage.put(SESSION_DATA_KEY, JsonUtils.toJson(this.data));
|
||||
|
||||
if (ICloudApiAuthenticationException.isAuthError(response.statusCode())) {
|
||||
throw new ICloudApiAuthenticationException(url, response.statusCode(), responseBodyAsString);
|
||||
} else if (response.statusCode() >= 300) {
|
||||
throw new ICloudApiResponseException(url, response.statusCode(), responseBodyAsString);
|
||||
}
|
||||
|
||||
return responseBodyAsString;
|
||||
}
|
||||
|
||||
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bouncycastle.crypto.CryptoException;
|
||||
import org.bouncycastle.crypto.Digest;
|
||||
import org.bouncycastle.crypto.agreement.srp.SRP6Util;
|
||||
import org.bouncycastle.crypto.digests.SHA256Digest;
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.icloud.internal.utilities.JsonUtils;
|
||||
import org.openhab.binding.icloud.internal.utilities.Pair;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
/**
|
||||
*
|
||||
* SrpAuthentication implements the SRP authentication for iCloud.
|
||||
*
|
||||
* @author Simon Spielmann - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SrpAuthentication {
|
||||
|
||||
private final String password;
|
||||
private final List<Pair<String, String>> sessionHeaders;
|
||||
|
||||
// N and g values from RFC 5054 - 2048 bit group
|
||||
private static final BigInteger N = new BigInteger(
|
||||
"2176617445861743577319100889180275378190766837425553851114464322"
|
||||
+ "4689886235383840957210909013086056401571399717235807266581649606"
|
||||
+ "4721484102914133641521973644771808873956554837381150726774022351"
|
||||
+ "0176252190156982074029314952962041933326626207347105454836873603"
|
||||
+ "9519702486226506248861060256971802984953561121442680157668000761"
|
||||
+ "4299882224570904138739739701719270939921147517651680636147611196"
|
||||
+ "1547623342209644278311797123637164733387141433589577347466730896"
|
||||
+ "7050807005509320424799678417036867928316761272274230314067548291"
|
||||
+ "1335824795830614395775593471019617714061736843785227034834953370"
|
||||
+ "37655006751328447510550299250924469288819");
|
||||
private final static BigInteger g = BigInteger.valueOf(2L);
|
||||
|
||||
// Username
|
||||
private String I;
|
||||
|
||||
private static MessageDigest md;
|
||||
static {
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new ExceptionInInitializerError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements SRP authentication according to Apple's specifications.
|
||||
*
|
||||
* @param accountName the account name (username)
|
||||
* @param password user password
|
||||
* @param sessionHeaders list of session headers
|
||||
*/
|
||||
public SrpAuthentication(String accountName, String password, List<Pair<String, String>> sessionHeaders) {
|
||||
this.I = accountName;
|
||||
this.password = password;
|
||||
this.sessionHeaders = sessionHeaders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert BigInteger to byte array without leading zero byte
|
||||
*
|
||||
* @param data BigInteger to convert
|
||||
* @return byte array representation of BigInteger without leading zero byte
|
||||
*/
|
||||
private static byte[] toByteArray(BigInteger data) {
|
||||
byte[] signedBytes = data.toByteArray();
|
||||
if (signedBytes[0] == 0x00) {
|
||||
// Removing leading zero byte
|
||||
byte[] unsignedBytes = new byte[signedBytes.length - 1];
|
||||
System.arraycopy(signedBytes, 1, unsignedBytes, 0, unsignedBytes.length);
|
||||
return unsignedBytes;
|
||||
} else {
|
||||
return signedBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform SRP authentication
|
||||
*
|
||||
* @param authEndpoint the authentication endpoint URL
|
||||
* @param httpClient the HTTP client session
|
||||
*/
|
||||
public void auth(String authEndpoint, ICloudSession httpClient) throws IOException, InterruptedException,
|
||||
ICloudApiResponseException, CryptoException, NoSuchAlgorithmException {
|
||||
SrpPassword srpPassword = new SrpPassword(password);
|
||||
|
||||
byte[] clientA = new byte[256];
|
||||
new SecureRandom().nextBytes(clientA);
|
||||
|
||||
BigInteger a = new BigInteger(1, clientA);
|
||||
BigInteger A = g.modPow(a, N);
|
||||
|
||||
// Prepare initial authentication request
|
||||
Map<String, Object> initData = Map.of("a", b64Encode(A), "accountName", I, "protocols",
|
||||
new String[] { "s2k", "s2k_fo" });
|
||||
|
||||
String initResponse = httpClient.post(authEndpoint + "/signin/init", JsonUtils.toJson(initData),
|
||||
sessionHeaders);
|
||||
|
||||
// Parse response
|
||||
JsonObject initBody = parseJson(initResponse);
|
||||
|
||||
if (initBody == null) {
|
||||
throw new ICloudApiResponseException("Failed to parse SRP init response", 520, "");
|
||||
}
|
||||
|
||||
BigInteger B = new BigInteger(1, b64Decode(initBody.get("b").getAsString()));
|
||||
byte[] s = b64Decode(initBody.get("salt").getAsString());
|
||||
String c = initBody.get("c").getAsString();
|
||||
int iterations = initBody.get("iteration").getAsInt();
|
||||
int keyLength = 32;
|
||||
|
||||
srpPassword.setEncryptInfo(s, iterations, keyLength);
|
||||
|
||||
// SRP-6a safety check
|
||||
if (B.mod(N).equals(BigInteger.ZERO)) {
|
||||
throw new CryptoException("Invalid server public value B");
|
||||
}
|
||||
|
||||
// Calculate S
|
||||
Digest digest = new SHA256Digest();
|
||||
BigInteger x = SRP6Util.calculateX(digest, N, s, "".getBytes(StandardCharsets.UTF_8), srpPassword.encode());
|
||||
BigInteger u = SRP6Util.calculateU(digest, N, A, B);
|
||||
|
||||
BigInteger k = SRP6Util.calculateK(digest, N, g);
|
||||
|
||||
BigInteger v = g.modPow(x, N);
|
||||
BigInteger S = B.subtract(k.multiply(v)).modPow(a.add(u.multiply(x)), N);
|
||||
|
||||
byte[] K = sha256(toUnsigned(S, 256));
|
||||
|
||||
// Compute client proof M1 = H(H(N) xor H(g) || H(I) || s || A || B || K)
|
||||
byte[] HN = sha256(toUnsigned(N, 256));
|
||||
byte[] Hg = sha256(toUnsigned(g, 256));
|
||||
byte[] Hxor = xor(HN, Hg);
|
||||
byte[] HI = sha256(I.getBytes(StandardCharsets.UTF_8));
|
||||
byte[] M1 = sha256(concat(Hxor, HI, s, toUnsigned(A, 256), toUnsigned(B, 256), K));
|
||||
|
||||
// Compute expected server proof M2 = H(A || M1 || K)
|
||||
byte[] M2 = sha256(concat(toUnsigned(A, 256), M1, K));
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("accountName", I);
|
||||
requestBody.put("c", c);
|
||||
requestBody.put("m1", b64Encode(M1));
|
||||
requestBody.put("m2", b64Encode(M2));
|
||||
requestBody.put("rememberMe", true);
|
||||
requestBody.put("trustTokens", new String[] { httpClient.getTrustToken() });
|
||||
httpClient.post(authEndpoint + "/signin/complete?isRememberMeEnabled=true", JsonUtils.toJson(requestBody),
|
||||
sessionHeaders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JSON response using Gson
|
||||
*
|
||||
* @param jsonString the JSON response string
|
||||
* @return the parsed JsonObject
|
||||
*/
|
||||
private @Nullable JsonObject parseJson(String jsonString) {
|
||||
Gson gson = new Gson();
|
||||
return gson.fromJson(jsonString, JsonObject.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the SHA-256 hash of the given data.
|
||||
*
|
||||
* @param data the input data.
|
||||
* @return the SHA-256 hash as a byte array.
|
||||
* @throws NoSuchAlgorithmException if SHA-256 algorithm is not available.
|
||||
*/
|
||||
private static byte[] sha256(byte[] data) {
|
||||
md.reset();
|
||||
return md.digest(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a BigInteger to an unsigned byte array of the specified length.
|
||||
* If the byte array representation of the BigInteger is shorter than the specified length,
|
||||
* it is left-padded with zeros. If it is longer, an exception is thrown.
|
||||
*
|
||||
* @param bigInteger the BigInteger to convert.
|
||||
* @param length the desired length of the resulting byte array.
|
||||
* @return a byte array of the given length representing the unsigned BigInteger.
|
||||
*/
|
||||
private static byte[] toUnsigned(BigInteger bigInteger, int length) {
|
||||
byte[] raw = bigInteger.toByteArray();
|
||||
if (raw.length == length && raw[0] != 0) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
byte[] unsigned;
|
||||
if (raw[0] == 0) {
|
||||
// strip leading sign byte
|
||||
unsigned = new byte[raw.length - 1];
|
||||
System.arraycopy(raw, 1, unsigned, 0, unsigned.length);
|
||||
} else {
|
||||
unsigned = raw;
|
||||
}
|
||||
|
||||
if (unsigned.length == length) {
|
||||
return unsigned;
|
||||
}
|
||||
|
||||
// pad to fixed length
|
||||
byte[] padded = new byte[length];
|
||||
System.arraycopy(unsigned, 0, padded, length - unsigned.length, unsigned.length);
|
||||
return padded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates multiple byte arrays into a single byte array.
|
||||
*
|
||||
* @param parts the byte arrays to concatenate
|
||||
* @return the concatenated byte array
|
||||
*/
|
||||
private static byte[] concat(byte[]... parts) {
|
||||
int total = Arrays.stream(parts).mapToInt(p -> p.length).sum();
|
||||
byte[] out = new byte[total];
|
||||
int pos = 0;
|
||||
for (byte[] p : parts) {
|
||||
System.arraycopy(p, 0, out, pos, p.length);
|
||||
pos += p.length;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* XORs two byte arrays of the same length.
|
||||
*
|
||||
* @param a first byte array
|
||||
* @param b second byte array
|
||||
* @return the result of XORing the two byte arrays
|
||||
*/
|
||||
private static byte[] xor(byte[] a, byte[] b) {
|
||||
assert (a.length == b.length);
|
||||
byte[] result = new byte[a.length];
|
||||
for (int i = 0; i < a.length; i++) {
|
||||
result[i] = (byte) (a[i] ^ b[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 encode
|
||||
*
|
||||
* @param data BigInteger to encode
|
||||
*/
|
||||
private static String b64Encode(BigInteger data) {
|
||||
return b64Encode(toByteArray(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 encode
|
||||
*
|
||||
* @param data byte array to encode
|
||||
*/
|
||||
private static String b64Encode(byte[] data) {
|
||||
return Base64.toBase64String(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base64 decode
|
||||
*
|
||||
* @param data string to decode
|
||||
*/
|
||||
private static byte[] b64Decode(String data) {
|
||||
return Base64.decode(data);
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import org.bouncycastle.crypto.digests.SHA256Digest;
|
||||
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
|
||||
/**
|
||||
* SrpPassword represents a password for SRP authentication.
|
||||
*
|
||||
* @author Simon Spielmann - Initial contribution
|
||||
*/
|
||||
public class SrpPassword {
|
||||
private final byte[] passwordHash;
|
||||
private byte[] salt;
|
||||
private Integer iterations;
|
||||
private Integer keyLength;
|
||||
|
||||
/**
|
||||
* Constructor for SrpPassword.
|
||||
*
|
||||
* @param password the password as a String
|
||||
*/
|
||||
public SrpPassword(String password) {
|
||||
this.passwordHash = sha256(password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the SHA-256 hash of the input string.
|
||||
*
|
||||
* @param input the input string
|
||||
* @return the SHA-256 hash as a byte array
|
||||
*/
|
||||
private byte[] sha256(String input) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
return digest.digest(input.getBytes(StandardCharsets.UTF_8));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("SHA-256 algorithm not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the encryption information.
|
||||
*
|
||||
* @param salt The salt
|
||||
* @param iterations Number of iterations
|
||||
* @param keyLength Key length
|
||||
*/
|
||||
public void setEncryptInfo(byte[] salt, int iterations, int keyLength) {
|
||||
this.salt = salt;
|
||||
this.iterations = iterations;
|
||||
this.keyLength = keyLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the password using PBKDF2 with the provided encryption information.
|
||||
*
|
||||
* @return The encoded password as a byte array
|
||||
*/
|
||||
public byte[] encode() {
|
||||
if (salt == null || iterations == null || keyLength == null) {
|
||||
throw new IllegalStateException("Encrypt info not set");
|
||||
}
|
||||
try {
|
||||
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA256Digest());
|
||||
gen.init(passwordHash, salt, iterations);
|
||||
KeyParameter key = (KeyParameter) gen.generateDerivedParameters(keyLength * 8);
|
||||
return key.getKey();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error during PBKDF2 encoding", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -45,8 +45,6 @@ import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* Retrieves the data for a given account from iCloud and passes the information to
|
||||
* {@link org.openhab.binding.icloud.internal.discovery.ICloudDeviceDiscovery} and to the {@link ICloudDeviceHandler}s.
|
||||
@@ -404,7 +402,7 @@ public class ICloudAccountBridgeHandler extends BaseBridgeHandler {
|
||||
"Status = " + statusCode + ", Response = " + json);
|
||||
}
|
||||
logger.debug("iCloud bridge data refresh complete.");
|
||||
} catch (NumberFormatException | JsonSyntaxException e) {
|
||||
} catch (RuntimeException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"iCloud response invalid: " + e.getMessage());
|
||||
}
|
||||
|
||||
+20
-1
@@ -18,11 +18,13 @@ import static org.openhab.binding.icloud.internal.ICloudBindingConstants.THING_T
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Scanner;
|
||||
|
||||
import org.bouncycastle.crypto.CryptoException;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -31,6 +33,7 @@ import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
|
||||
import org.openhab.binding.icloud.internal.ICloudApiResponseException;
|
||||
import org.openhab.binding.icloud.internal.ICloudBindingConstants;
|
||||
import org.openhab.binding.icloud.internal.ICloudService;
|
||||
import org.openhab.binding.icloud.internal.SrpPassword;
|
||||
import org.openhab.binding.icloud.internal.handler.ICloudDeviceHandler;
|
||||
import org.openhab.binding.icloud.internal.handler.dto.json.response.ICloudAccountDataResponse;
|
||||
import org.openhab.binding.icloud.internal.utilities.JsonUtils;
|
||||
@@ -87,9 +90,25 @@ public class TestICloud {
|
||||
iCloudTestPassword = sysPropPW != null ? sysPropPW : "notset";
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSrpPassword() {
|
||||
String password = "testpassword";
|
||||
byte[] salt = new byte[] { 1, 2, 3, 4 };
|
||||
|
||||
int iterations = 20622;
|
||||
int keyLength = 32;
|
||||
SrpPassword srpPassword = new SrpPassword(password);
|
||||
srpPassword.setEncryptInfo(salt, iterations, keyLength);
|
||||
|
||||
byte[] expected = new byte[] { 66, -77, 114, 66, -54, -84, 100, 100, 77, 71, -77, 83, -6, -42, 88, 43, -78, 95,
|
||||
35, 45, -105, 111, -9, 106, 12, -89, -111, 63, -36, -34, -101, -104 };
|
||||
assertArrayEquals(expected, srpPassword.encode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledIfSystemProperty(named = "icloud.test.email", matches = ".*", disabledReason = "Only for manual execution.")
|
||||
public void testAuth() throws IOException, InterruptedException, ICloudApiResponseException, JsonSyntaxException {
|
||||
public void testAuth() throws IOException, InterruptedException, ICloudApiResponseException, JsonSyntaxException,
|
||||
CryptoException, NoSuchAlgorithmException {
|
||||
File jsonStorageFile = new File(System.getProperty("user.home"), "openhab.json");
|
||||
logger.info(jsonStorageFile.toString());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user