]> git.basschouten.com Git - openhab-addons.git/blob
840b0f758e122ce6772165f308271b5695a75f8e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.miio.internal.cloud;
14
15 import static org.openhab.binding.miio.internal.MiIoBindingConstants.BINDING_USERDATA_PATH;
16
17 import java.io.ByteArrayOutputStream;
18 import java.io.DataOutputStream;
19 import java.io.File;
20 import java.io.FileWriter;
21 import java.io.IOException;
22 import java.net.URI;
23 import java.nio.charset.StandardCharsets;
24 import java.nio.file.Files;
25 import java.nio.file.Path;
26 import java.nio.file.Paths;
27 import java.util.ArrayList;
28 import java.util.Base64;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Random;
32 import java.util.Set;
33 import java.util.TreeMap;
34
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.openhab.binding.miio.internal.MiIoCryptoException;
38 import org.slf4j.Logger;
39
40 /**
41  * The {@link CloudUtil} class is used for supporting functions for Xiaomi cloud access
42  *
43  * @author Marcel Verpaalen - Initial contribution
44  */
45 @NonNullByDefault
46 public class CloudUtil {
47
48     private static final Random RANDOM = new Random();
49
50     /**
51      * Saves the Xiaomi cloud device info with tokens to file
52      *
53      * @param data file content
54      * @param country county server
55      * @param logger
56      */
57     public static void saveDeviceInfoFile(String data, String country, Logger logger) {
58         File folder = new File(BINDING_USERDATA_PATH);
59         if (!folder.exists()) {
60             folder.mkdirs();
61         }
62         File dataFile = new File(folder, "miioTokens-" + country + ".json");
63         try (FileWriter writer = new FileWriter(dataFile)) {
64             writer.write(data);
65             logger.debug("Devices token info saved to {}", dataFile.getAbsolutePath());
66         } catch (IOException e) {
67             logger.debug("Failed to write token file '{}': {}", dataFile.getName(), e.getMessage());
68         }
69     }
70
71     /**
72      * Generate signature for the request.
73      *
74      * @param method http request method. GET or POST
75      * @param requestUrl the full request url. e.g.: http://api.xiaomi.com/getUser?id=123321
76      * @param params request params. This should be a TreeMap because the
77      *            parameters are required to be in lexicographic order.
78      * @param signedNonce secret key for encryption.
79      * @return hash value for the values provided
80      * @throws MiIoCryptoException
81      */
82     public static String generateSignature(@Nullable String requestUrl, @Nullable String signedNonce, String nonce,
83             @Nullable Map<String, String> params) throws MiIoCryptoException {
84         if (signedNonce == null || signedNonce.length() == 0) {
85             throw new MiIoCryptoException("key is not nullable");
86         }
87         List<String> exps = new ArrayList<String>();
88
89         if (requestUrl != null) {
90             URI uri = URI.create(requestUrl);
91             exps.add(uri.getPath());
92         }
93         exps.add(signedNonce);
94         exps.add(nonce);
95
96         if (params != null && !params.isEmpty()) {
97             final TreeMap<String, String> sortedParams = new TreeMap<String, String>(params);
98             Set<Map.Entry<String, String>> entries = sortedParams.entrySet();
99             for (Map.Entry<String, String> entry : entries) {
100                 exps.add(String.format("%s=%s", entry.getKey(), entry.getValue()));
101             }
102         }
103         boolean first = true;
104         StringBuilder sb = new StringBuilder();
105         for (String s : exps) {
106             if (!first) {
107                 sb.append('&');
108             } else {
109                 first = false;
110             }
111             sb.append(s);
112         }
113         return CloudCrypto.hMacSha256Encode(Base64.getDecoder().decode(signedNonce),
114                 sb.toString().getBytes(StandardCharsets.UTF_8));
115     }
116
117     public static String generateNonce(long milli) throws IOException {
118         ByteArrayOutputStream output = new ByteArrayOutputStream();
119         DataOutputStream dataOutputStream = new DataOutputStream(output);
120         dataOutputStream.writeLong(RANDOM.nextLong());
121         dataOutputStream.writeInt((int) (milli / 60000));
122         dataOutputStream.flush();
123         return Base64.getEncoder().encodeToString(output.toByteArray());
124     }
125
126     public static String signedNonce(String ssecret, String nonce) throws IOException, MiIoCryptoException {
127         byte[] byteArrayS = Base64.getDecoder().decode(ssecret.getBytes(StandardCharsets.UTF_8));
128         byte[] byteArrayN = Base64.getDecoder().decode(nonce.getBytes(StandardCharsets.UTF_8));
129         ByteArrayOutputStream output = new ByteArrayOutputStream();
130         output.write(byteArrayS);
131         output.write(byteArrayN);
132         return CloudCrypto.sha256Hash(output.toByteArray());
133     }
134
135     public static void writeBytesToFileNio(byte[] bFile, String fileDest) throws IOException {
136         Path path = Paths.get(fileDest);
137         Files.write(path, bFile);
138     }
139 }