/* Copyright (C) 2023-2024 Andreas Shimokawa, José Rebelo This file is part of Gadgetbridge. Gadgetbridge is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Gadgetbridge is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi; import android.content.SharedPreferences; import android.os.Build; import androidx.annotation.Nullable; import com.google.protobuf.ByteString; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.bouncycastle.shaded.crypto.CryptoException; import org.bouncycastle.shaded.crypto.engines.AESEngine; import org.bouncycastle.shaded.crypto.modes.CCMBlockCipher; import org.bouncycastle.shaded.crypto.params.AEADParameters; import org.bouncycastle.shaded.crypto.params.KeyParameter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; import java.util.Locale; import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import nodomain.freeyourgadget.gadgetbridge.BuildConfig; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.AbstractXiaomiService; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class XiaomiAuthService extends AbstractXiaomiService { private static final Logger LOG = LoggerFactory.getLogger(XiaomiAuthService.class); public static final int COMMAND_TYPE = 1; public static final int CMD_SEND_USERID = 5; public static final int CMD_NONCE = 26; public static final int CMD_AUTH = 27; private boolean encryptionInitialized = false; private final byte[] secretKey = new byte[16]; private final byte[] nonce = new byte[16]; private final byte[] encryptionKey = new byte[16]; private final byte[] decryptionKey = new byte[16]; private final byte[] encryptionNonce = new byte[4]; private final byte[] decryptionNonce = new byte[4]; public XiaomiAuthService(final XiaomiSupport support) { super(support); } public boolean isEncryptionInitialized() { return encryptionInitialized; } protected void startEncryptedHandshake() { encryptionInitialized = false; System.arraycopy(getSecretKey(getSupport().getDevice()), 0, secretKey, 0, 16); new SecureRandom().nextBytes(nonce); getSupport().sendCommand("auth step 1", buildNonceCommand(nonce)); } protected void startClearTextHandshake() { final XiaomiProto.Auth auth = XiaomiProto.Auth.newBuilder() .setUserId(getUserId(getSupport().getDevice())) .build(); final XiaomiProto.Command command = XiaomiProto.Command.newBuilder() .setType(XiaomiAuthService.COMMAND_TYPE) .setSubtype(XiaomiAuthService.CMD_SEND_USERID) .setAuth(auth) .build(); getSupport().sendCommand("auth step 1", command); } @Override public void handleCommand(final XiaomiProto.Command cmd) { if (cmd.getType() != COMMAND_TYPE) { throw new IllegalArgumentException("Not an auth command"); } switch (cmd.getSubtype()) { case CMD_NONCE: { LOG.debug("Got watch nonce"); // Watch nonce final XiaomiProto.Command command = handleWatchNonce(cmd.getAuth().getWatchNonce()); if (command == null) { LOG.error("handleWatchNonce returned null, disconnecting"); final GBDevice device = getSupport().getDevice(); if (device != null) { GBApplication.deviceService(device).disconnect(); } return; } getSupport().sendCommand("auth step 2", command); break; } case CMD_AUTH: case CMD_SEND_USERID: { if (cmd.getSubtype() == CMD_AUTH || cmd.getAuth().getStatus() == 1) { encryptionInitialized = cmd.getSubtype() == CMD_AUTH; LOG.info("Authenticated, further communications are {}", encryptionInitialized ? "encrypted" : "in plaintext"); getSupport().getDevice().setState(GBDevice.State.INITIALIZED); getSupport().getDevice().sendDeviceUpdateIntent(getSupport().getContext(), GBDevice.DeviceUpdateSubject.DEVICE_STATE); getSupport().onAuthSuccess(); } else { LOG.warn("could not authenticate"); } break; } default: LOG.warn("Unknown auth payload subtype {}", cmd.getSubtype()); } } public byte[] encrypt(final byte[] arr, final int i) { final ByteBuffer packetNonce = ByteBuffer.allocate(12).order(ByteOrder.LITTLE_ENDIAN) .put(encryptionNonce) .putInt(0) .putInt(i); try { return encrypt(encryptionKey, packetNonce.array(), arr); } catch (final CryptoException e) { throw new RuntimeException("failed to encrypt", e); } } public byte[] decrypt(final byte[] arr) { final ByteBuffer packetNonce = ByteBuffer.allocate(12).order(ByteOrder.LITTLE_ENDIAN); packetNonce.put(decryptionNonce); packetNonce.putInt(0); packetNonce.putInt(0); try { return decrypt(decryptionKey, packetNonce.array(), arr); } catch (final CryptoException e) { throw new RuntimeException("failed to decrypt", e); } } @Nullable private XiaomiProto.Command handleWatchNonce(final XiaomiProto.WatchNonce watchNonce) { final byte[] step2hmac = computeAuthStep3Hmac(secretKey, nonce, watchNonce.getNonce().toByteArray()); System.arraycopy(step2hmac, 0, decryptionKey, 0, 16); System.arraycopy(step2hmac, 16, encryptionKey, 0, 16); System.arraycopy(step2hmac, 32, decryptionNonce, 0, 4); System.arraycopy(step2hmac, 36, encryptionNonce, 0, 4); if (BuildConfig.DEBUG) { LOG.debug("decryptionKey: {}", GB.hexdump(decryptionKey)); LOG.debug("encryptionKey: {}", GB.hexdump(encryptionKey)); LOG.debug("decryptionNonce: {}", GB.hexdump(decryptionNonce)); LOG.debug("encryptionNonce: {}", GB.hexdump(encryptionNonce)); } final byte[] decryptionConfirmation = hmacSHA256(decryptionKey, ArrayUtils.addAll(watchNonce.getNonce().toByteArray(), nonce)); if (!Arrays.equals(decryptionConfirmation, watchNonce.getHmac().toByteArray())) { LOG.warn("Watch hmac mismatch"); return null; } final XiaomiProto.AuthDeviceInfo authDeviceInfo = XiaomiProto.AuthDeviceInfo.newBuilder() .setUnknown1(0) // TODO ? .setPhoneApiLevel(Build.VERSION.SDK_INT) .setPhoneName(Build.MODEL) .setUnknown3(224) // TODO ? // TODO region should be actual device region? .setRegion(Locale.getDefault().getLanguage().substring(0, 2).toUpperCase(Locale.ROOT)) .build(); final byte[] encryptedNonces = hmacSHA256(encryptionKey, ArrayUtils.addAll(nonce, watchNonce.getNonce().toByteArray())); final byte[] encryptedDeviceInfo = encrypt(authDeviceInfo.toByteArray(), 0); final XiaomiProto.AuthStep3 authStep3 = XiaomiProto.AuthStep3.newBuilder() .setEncryptedNonces(ByteString.copyFrom(encryptedNonces)) .setEncryptedDeviceInfo(ByteString.copyFrom(encryptedDeviceInfo)) .build(); final XiaomiProto.Command.Builder cmd = XiaomiProto.Command.newBuilder(); cmd.setType(COMMAND_TYPE); cmd.setSubtype(CMD_AUTH); final XiaomiProto.Auth.Builder auth = XiaomiProto.Auth.newBuilder(); auth.setAuthStep3(authStep3); return cmd.setAuth(auth.build()).build(); } public static XiaomiProto.Command buildNonceCommand(final byte[] nonce) { final XiaomiProto.PhoneNonce.Builder phoneNonce = XiaomiProto.PhoneNonce.newBuilder(); phoneNonce.setNonce(ByteString.copyFrom(nonce)); final XiaomiProto.Auth.Builder auth = XiaomiProto.Auth.newBuilder(); auth.setPhoneNonce(phoneNonce.build()); final XiaomiProto.Command.Builder command = XiaomiProto.Command.newBuilder(); command.setType(COMMAND_TYPE); command.setSubtype(CMD_NONCE); command.setAuth(auth.build()); return command.build(); } public static byte[] computeAuthStep3Hmac(final byte[] secretKey, final byte[] phoneNonce, final byte[] watchNonce) { final byte[] miwearAuthBytes = "miwear-auth".getBytes(); final Mac mac; try { mac = Mac.getInstance("HmacSHA256"); // Compute the actual key and re-initialize the mac mac.init(new SecretKeySpec(ArrayUtils.addAll(phoneNonce, watchNonce), "HmacSHA256")); final byte[] hmacKeyBytes = mac.doFinal(secretKey); final SecretKeySpec key = new SecretKeySpec(hmacKeyBytes, "HmacSHA256"); mac.init(key); } catch (final NoSuchAlgorithmException | InvalidKeyException e) { throw new IllegalStateException("Failed to initialize hmac for auth step 2", e); } final byte[] output = new byte[64]; byte[] tmp = new byte[0]; byte b = 1; int i = 0; while (i < output.length) { mac.update(tmp); mac.update(miwearAuthBytes); mac.update(b); tmp = mac.doFinal(); for (int j = 0; j < tmp.length && i < output.length; j++, i++) { output[i] = tmp[j]; } b++; } return output; } protected static byte[] getSecretKey(final GBDevice device) { final byte[] authKeyBytes = new byte[16]; final SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()); final String authKey = sharedPrefs.getString("authkey", "").trim(); if (StringUtils.isNotBlank(authKey)) { final byte[] srcBytes; // Allow both with and without 0x, to avoid user mistakes if (authKey.length() == 34 && authKey.startsWith("0x")) { srcBytes = GB.hexStringToByteArray(authKey.trim().substring(2)); } else { srcBytes = GB.hexStringToByteArray(authKey.trim()); } System.arraycopy(srcBytes, 0, authKeyBytes, 0, Math.min(srcBytes.length, 16)); } return authKeyBytes; } protected static String getUserId(final GBDevice device) { final SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()); final String authKey = sharedPrefs.getString("authkey", null); if (StringUtils.isNotBlank(authKey)) { return authKey; } return "0000000000"; } protected static byte[] hmacSHA256(final byte[] key, final byte[] input) { try { final Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(key, "HmacSHA256")); return mac.doFinal(input); } catch (final Exception e) { throw new RuntimeException("Failed to hmac", e); } } public static byte[] encrypt(final byte[] key, final byte[] nonce, final byte[] payload) throws CryptoException { final CCMBlockCipher cipher = createBlockCipher(true, new SecretKeySpec(key, "AES"), nonce); final byte[] out = new byte[cipher.getOutputSize(payload.length)]; final int outBytes = cipher.processBytes(payload, 0, payload.length, out, 0); cipher.doFinal(out, outBytes); return out; } public static byte[] decrypt(final byte[] key, final byte[] nonce, final byte[] encryptedPayload) throws CryptoException { final CCMBlockCipher cipher = createBlockCipher(false, new SecretKeySpec(key, "AES"), nonce); final byte[] decrypted = new byte[cipher.getOutputSize(encryptedPayload.length)]; cipher.doFinal(decrypted, cipher.processBytes(encryptedPayload, 0, encryptedPayload.length, decrypted, 0)); return decrypted; } public static CCMBlockCipher createBlockCipher(final boolean forEncrypt, final SecretKey secretKey, final byte[] nonce) { final AESEngine aesFastEngine = new AESEngine(); aesFastEngine.init(forEncrypt, new KeyParameter(secretKey.getEncoded())); final CCMBlockCipher blockCipher = new CCMBlockCipher(aesFastEngine); blockCipher.init(forEncrypt, new AEADParameters(new KeyParameter(secretKey.getEncoded()), 32, nonce, null)); return blockCipher; } }