2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.miio.internal.cloud;
15 import java.io.ByteArrayOutputStream;
16 import java.io.DataOutputStream;
18 import java.io.FileWriter;
19 import java.io.IOException;
21 import java.nio.charset.StandardCharsets;
22 import java.nio.file.Files;
23 import java.nio.file.Path;
24 import java.nio.file.Paths;
25 import java.util.ArrayList;
26 import java.util.Base64;
27 import java.util.List;
29 import java.util.Random;
31 import java.util.TreeMap;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.openhab.binding.miio.internal.MiIoBindingConstants;
36 import org.openhab.binding.miio.internal.MiIoCryptoException;
37 import org.openhab.core.config.core.ConfigConstants;
38 import org.slf4j.Logger;
41 * The {@link CloudUtil} class is used for supporting functions for Xiaomi cloud access
43 * @author Marcel Verpaalen - Initial contribution
46 public class CloudUtil {
48 private static final Random RANDOM = new Random();
49 private static final String DB_FOLDER_NAME = ConfigConstants.getUserDataFolder() + File.separator
50 + MiIoBindingConstants.BINDING_ID;
53 * Saves the Xiaomi cloud device info with tokens to file
55 * @param data file content
56 * @param country county server
59 public static void saveDeviceInfoFile(String data, String country, Logger logger) {
60 File folder = new File(DB_FOLDER_NAME);
61 if (!folder.exists()) {
64 File dataFile = new File(folder, "miioTokens-" + country + ".json");
65 try (FileWriter writer = new FileWriter(dataFile)) {
67 logger.debug("Devices token info saved to {}", dataFile.getAbsolutePath());
68 } catch (IOException e) {
69 logger.debug("Failed to write token file '{}': {}", dataFile.getName(), e.getMessage());
74 * Generate signature for the request.
76 * @param method http request method. GET or POST
77 * @param requestUrl the full request url. e.g.: http://api.xiaomi.com/getUser?id=123321
78 * @param params request params. This should be a TreeMap because the
79 * parameters are required to be in lexicographic order.
80 * @param signedNonce secret key for encryption.
81 * @return hash value for the values provided
82 * @throws MiIoCryptoException
84 public static String generateSignature(@Nullable String requestUrl, @Nullable String signedNonce, String nonce,
85 @Nullable Map<String, String> params) throws MiIoCryptoException {
86 if (signedNonce == null || signedNonce.length() == 0) {
87 throw new MiIoCryptoException("key is not nullable");
89 List<String> exps = new ArrayList<String>();
91 if (requestUrl != null) {
92 URI uri = URI.create(requestUrl);
93 exps.add(uri.getPath());
95 exps.add(signedNonce);
98 if (params != null && !params.isEmpty()) {
99 final TreeMap<String, String> sortedParams = new TreeMap<String, String>(params);
100 Set<Map.Entry<String, String>> entries = sortedParams.entrySet();
101 for (Map.Entry<String, String> entry : entries) {
102 exps.add(String.format("%s=%s", entry.getKey(), entry.getValue()));
105 boolean first = true;
106 StringBuilder sb = new StringBuilder();
107 for (String s : exps) {
115 return CloudCrypto.hMacSha256Encode(Base64.getDecoder().decode(signedNonce),
116 sb.toString().getBytes(StandardCharsets.UTF_8));
119 public static String generateNonce(long milli) throws IOException {
120 ByteArrayOutputStream output = new ByteArrayOutputStream();
121 DataOutputStream dataOutputStream = new DataOutputStream(output);
122 dataOutputStream.writeLong(RANDOM.nextLong());
123 dataOutputStream.writeInt((int) (milli / 60000));
124 dataOutputStream.flush();
125 return Base64.getEncoder().encodeToString(output.toByteArray());
128 public static String signedNonce(String ssecret, String nonce) throws IOException, MiIoCryptoException {
129 byte[] byteArrayS = Base64.getDecoder().decode(ssecret.getBytes(StandardCharsets.UTF_8));
130 byte[] byteArrayN = Base64.getDecoder().decode(nonce.getBytes(StandardCharsets.UTF_8));
131 ByteArrayOutputStream output = new ByteArrayOutputStream();
132 output.write(byteArrayS);
133 output.write(byteArrayN);
134 return CloudCrypto.sha256Hash(output.toByteArray());
137 public static void writeBytesToFileNio(byte[] bFile, String fileDest) throws IOException {
138 Path path = Paths.get(fileDest);
139 Files.write(path, bFile);