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