1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-24 10:56:50 +01:00

CMF Watch Pro 2: Negotiate authentication key

This commit is contained in:
José Rebelo 2024-08-19 10:15:13 +01:00
parent 7514b50d19
commit cc5eadbc62
4 changed files with 179 additions and 38 deletions

View File

@ -41,6 +41,12 @@ public class CmfWatchPro2Coordinator extends CmfWatchProCoordinator {
return R.drawable.ic_device_watchxplus_disabled; return R.drawable.ic_device_watchxplus_disabled;
} }
@Override
public int getBondingStyle() {
// We can negotiate auth key - #3982
return BONDING_STYLE_BOND;
}
@Override @Override
public boolean supportsSunriseSunset() { public boolean supportsSunriseSunset() {
return true; return true;

View File

@ -182,6 +182,8 @@ public class CmfCharacteristic {
private boolean shouldEncrypt(final CmfCommand cmd) { private boolean shouldEncrypt(final CmfCommand cmd) {
switch (cmd) { switch (cmd) {
case AUTH_PAIR_REQUEST:
case AUTH_PAIR_REPLY:
case DATA_CHUNK_WRITE_AGPS: case DATA_CHUNK_WRITE_AGPS:
case DATA_CHUNK_WRITE_WATCHFACE: case DATA_CHUNK_WRITE_WATCHFACE:
return false; return false;
@ -199,7 +201,7 @@ public class CmfCharacteristic {
return; return;
} }
final int encryptedPayloadLength = buf.getShort(); final int payloadLength = buf.getShort();
final int cmd1 = buf.getShort() & 0xFFFF; final int cmd1 = buf.getShort() & 0xFFFF;
final int chunkCount = buf.getShort(); final int chunkCount = buf.getShort();
final int chunkIndex = buf.getShort(); final int chunkIndex = buf.getShort();
@ -208,23 +210,29 @@ public class CmfCharacteristic {
final CmfCommand cmd = CmfCommand.fromCodes(cmd1, cmd2); final CmfCommand cmd = CmfCommand.fromCodes(cmd1, cmd2);
final byte[] payload; final byte[] payload;
if (encryptedPayloadLength > 0) { if (payloadLength > 0) {
final byte[] encryptedPayload = new byte[encryptedPayloadLength];
buf.get(encryptedPayload);
try { try {
final byte[] decryptedPayload = CryptoUtils.decryptAES_CBC_Pad(encryptedPayload, sessionKey, AES_IV); if (cmd == null || shouldEncrypt(cmd)) {
payload = ArrayUtils.subarray(decryptedPayload, 0, decryptedPayload.length - 4); final byte[] encryptedPayload = new byte[payloadLength];
final int expectedCrc = BLETypeConversions.toUint32(decryptedPayload, decryptedPayload.length - 4); buf.get(encryptedPayload);
final CRC32 crc = new CRC32();
crc.update(payload, 0, payload.length); final byte[] decryptedPayload = CryptoUtils.decryptAES_CBC_Pad(encryptedPayload, sessionKey, AES_IV);
final int actualCrc = (int) crc.getValue(); payload = ArrayUtils.subarray(decryptedPayload, 0, decryptedPayload.length - 4);
if (actualCrc != expectedCrc) { final int expectedCrc = BLETypeConversions.toUint32(decryptedPayload, decryptedPayload.length - 4);
LOG.error("Payload CRC mismatch for {}: got {}, expected {}", cmd, String.format("%08X", actualCrc), String.format("%08X", expectedCrc)); final CRC32 crc = new CRC32();
if (chunkCount > 1) { crc.update(payload, 0, payload.length);
chunkBuffers.remove(cmd); final int actualCrc = (int) crc.getValue();
if (actualCrc != expectedCrc) {
LOG.error("Payload CRC mismatch for {}: got {}, expected {}", cmd, String.format("%08X", actualCrc), String.format("%08X", expectedCrc));
if (chunkCount > 1) {
chunkBuffers.remove(cmd);
}
return;
} }
return; } else {
// Plaintext payload - it does not have the crc, but the length still includes it (?)
payload = new byte[buf.limit() - buf.position()];
buf.get(payload);
} }
} catch (final GeneralSecurityException e) { } catch (final GeneralSecurityException e) {
LOG.error("Failed to decrypt payload for {} ({}/{})", cmd, String.format("0x%04x", cmd1), String.format("0x%04x", cmd2), e); LOG.error("Failed to decrypt payload for {} ({}/{})", cmd, String.format("0x%04x", cmd1), String.format("0x%04x", cmd2), e);

View File

@ -27,6 +27,8 @@ import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable;
import net.e175.klaus.solarpositioning.DeltaT; import net.e175.klaus.solarpositioning.DeltaT;
import net.e175.klaus.solarpositioning.SPA; import net.e175.klaus.solarpositioning.SPA;
import net.e175.klaus.solarpositioning.SunriseTransitSet; import net.e175.klaus.solarpositioning.SunriseTransitSet;
@ -39,11 +41,12 @@ import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar; import java.util.Calendar;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.Random;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.UUID; import java.util.UUID;
@ -54,6 +57,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInf
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.CmfWatchProCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.CmfWatchProCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@ -87,6 +91,10 @@ public class CmfWatchProSupport extends AbstractBTLEDeviceSupport implements Cmf
public static final UUID UUID_CHARACTERISTIC_CMF_DATA_WRITE = UUID.fromString("02f00000-0000-0000-0000-00000000ffe1"); public static final UUID UUID_CHARACTERISTIC_CMF_DATA_WRITE = UUID.fromString("02f00000-0000-0000-0000-00000000ffe1");
public static final UUID UUID_CHARACTERISTIC_CMF_DATA_READ = UUID.fromString("02f00000-0000-0000-0000-00000000ffe2"); public static final UUID UUID_CHARACTERISTIC_CMF_DATA_READ = UUID.fromString("02f00000-0000-0000-0000-00000000ffe2");
public static final UUID UUID_SERVICE_CMF_SHELL = UUID.fromString("77d4e67c-2fe2-2334-0d35-9ccd078f529c");
public static final UUID UUID_CHARACTERISTIC_CMF_SHELL_WRITE = UUID.fromString("77d4ff01-2fe2-2334-0d35-9ccd078f529c");
public static final UUID UUID_CHARACTERISTIC_CMF_SHELL_READ = UUID.fromString("77d4ff02-2fe2-2334-0d35-9ccd078f529c");
// An a5 byte is used a lot in single payloads, probably as a "proof of encryption"? // An a5 byte is used a lot in single payloads, probably as a "proof of encryption"?
public static final byte A5 = (byte) 0xa5; public static final byte A5 = (byte) 0xa5;
@ -95,6 +103,9 @@ public class CmfWatchProSupport extends AbstractBTLEDeviceSupport implements Cmf
private CmfCharacteristic characteristicDataRead; private CmfCharacteristic characteristicDataRead;
private CmfCharacteristic characteristicDataWrite; private CmfCharacteristic characteristicDataWrite;
private final byte[] authRandom1 = new byte[16];
private final byte[] authAppSecret = new byte[16];
private final CmfActivitySync activitySync = new CmfActivitySync(this); private final CmfActivitySync activitySync = new CmfActivitySync(this);
private final CmfPreferences preferences = new CmfPreferences(this); private final CmfPreferences preferences = new CmfPreferences(this);
private CmfDataUploader dataUploader; private CmfDataUploader dataUploader;
@ -105,6 +116,7 @@ public class CmfWatchProSupport extends AbstractBTLEDeviceSupport implements Cmf
super(LOG); super(LOG);
addSupportedService(UUID_SERVICE_CMF_CMD); addSupportedService(UUID_SERVICE_CMF_CMD);
addSupportedService(UUID_SERVICE_CMF_DATA); addSupportedService(UUID_SERVICE_CMF_DATA);
addSupportedService(UUID_SERVICE_CMF_SHELL);
} }
@Override @Override
@ -143,6 +155,16 @@ public class CmfWatchProSupport extends AbstractBTLEDeviceSupport implements Cmf
return builder; return builder;
} }
final BluetoothGattCharacteristic btCharacteristicShellWrite = getCharacteristic(UUID_CHARACTERISTIC_CMF_SHELL_WRITE);
if (btCharacteristicShellWrite == null) {
LOG.warn("Characteristic shell write is null");
}
final BluetoothGattCharacteristic btCharacteristicShellRead = getCharacteristic(UUID_CHARACTERISTIC_CMF_SHELL_READ);
if (btCharacteristicShellRead == null) {
LOG.warn("Characteristic shell read is null");
}
dataUploader = new CmfDataUploader(this); dataUploader = new CmfDataUploader(this);
characteristicCommandRead = new CmfCharacteristic(btCharacteristicCommandRead, this); characteristicCommandRead = new CmfCharacteristic(btCharacteristicCommandRead, this);
@ -150,20 +172,29 @@ public class CmfWatchProSupport extends AbstractBTLEDeviceSupport implements Cmf
characteristicDataRead = new CmfCharacteristic(btCharacteristicDataRead, dataUploader); characteristicDataRead = new CmfCharacteristic(btCharacteristicDataRead, dataUploader);
characteristicDataWrite = new CmfCharacteristic(btCharacteristicDataWrite, null); characteristicDataWrite = new CmfCharacteristic(btCharacteristicDataWrite, null);
final byte[] secretKey = getSecretKey(getDevice());
characteristicCommandRead.setSessionKey(secretKey);
characteristicCommandWrite.setSessionKey(secretKey);
characteristicDataRead.setSessionKey(secretKey);
characteristicDataWrite.setSessionKey(secretKey);
builder.notify(btCharacteristicCommandWrite, true);
builder.notify(btCharacteristicCommandRead, true); builder.notify(btCharacteristicCommandRead, true);
builder.notify(btCharacteristicDataWrite, true);
builder.notify(btCharacteristicDataRead, true); builder.notify(btCharacteristicDataRead, true);
if (btCharacteristicShellRead != null) {
builder.notify(btCharacteristicShellRead, true);
}
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.AUTHENTICATING, getContext())); builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.AUTHENTICATING, getContext()));
sendCommand(builder, CmfCommand.AUTH_PHONE_NAME, ArrayUtils.addAll(new byte[]{A5}, Build.MODEL.getBytes(StandardCharsets.UTF_8))); final byte[] secretKey = getSecretKey(getDevice());
if (secretKey != null) {
characteristicCommandRead.setSessionKey(secretKey);
characteristicCommandWrite.setSessionKey(secretKey);
characteristicDataRead.setSessionKey(secretKey);
characteristicDataWrite.setSessionKey(secretKey);
sendCommand(builder, CmfCommand.AUTH_PHONE_NAME, ArrayUtils.addAll(new byte[]{A5}, Build.MODEL.getBytes(StandardCharsets.UTF_8)));
} else if (btCharacteristicShellWrite != null) {
builder.write(getCharacteristic(UUID_CHARACTERISTIC_CMF_SHELL_WRITE), "AT GETSECRET".getBytes());
} else {
GB.toast(getContext(), R.string.authentication_failed_check_key, Toast.LENGTH_LONG, GB.WARN);
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.NOT_CONNECTED, getContext()));
}
return builder; return builder;
} }
@ -191,6 +222,9 @@ public class CmfWatchProSupport extends AbstractBTLEDeviceSupport implements Cmf
} else if (characteristicUUID.equals(characteristicDataRead.getCharacteristicUUID())) { } else if (characteristicUUID.equals(characteristicDataRead.getCharacteristicUUID())) {
characteristicDataRead.onCharacteristicChanged(value); characteristicDataRead.onCharacteristicChanged(value);
return true; return true;
} else if (characteristicUUID.equals(UUID_CHARACTERISTIC_CMF_SHELL_READ)) {
handleShellCommand(value);
return true;
} }
LOG.warn("Unhandled characteristic changed: {} {}", characteristicUUID, GB.hexdump(value)); LOG.warn("Unhandled characteristic changed: {} {}", characteristicUUID, GB.hexdump(value));
@ -218,6 +252,51 @@ public class CmfWatchProSupport extends AbstractBTLEDeviceSupport implements Cmf
} }
switch (cmd) { switch (cmd) {
case AUTH_PAIR_REPLY:
final byte[] authRandom2 = ArrayUtils.subarray(payload, 0, 16);
final byte[] signedAuthRandom2 = ArrayUtils.subarray(payload, 16, 48);
LOG.debug("authRandom2: {}", GB.hexdump(authRandom2));
LOG.debug("signedAuthRandom2: {}", GB.hexdump(signedAuthRandom2));
try {
// Validate random2 signature
final MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
sha256.update(authRandom2);
sha256.update(authAppSecret);
byte[] random2verify = sha256.digest();
if (!Arrays.equals(signedAuthRandom2, random2verify)) {
LOG.error("random2 signature mismatch");
authNegotiationFailed();
return;
}
// Compute K1 and update preferences
sha256.reset();
sha256.update(authRandom1);
sha256.update(authRandom2);
sha256.update(authAppSecret);
final byte[] k1full = sha256.digest();
final byte[] secretKey = ArrayUtils.subarray(k1full, 0, 16);
LOG.debug("Negotiated K1: {}", k1full);
evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences("authkey", GB.hexdump(secretKey)));
characteristicCommandRead.setSessionKey(secretKey);
characteristicCommandWrite.setSessionKey(secretKey);
characteristicDataRead.setSessionKey(secretKey);
characteristicDataWrite.setSessionKey(secretKey);
sendCommand("auth step 2", CmfCommand.AUTH_PHONE_NAME, ArrayUtils.addAll(new byte[]{A5}, Build.MODEL.getBytes(StandardCharsets.UTF_8)));
} catch (final Exception e) {
LOG.error("Failed to negotiate K1", e);
authNegotiationFailed();
return;
}
return;
case AUTH_FAILED: case AUTH_FAILED:
LOG.error("Authentication failed, disconnecting"); LOG.error("Authentication failed, disconnecting");
GB.toast(getContext(), R.string.authentication_failed_check_key, Toast.LENGTH_LONG, GB.WARN); GB.toast(getContext(), R.string.authentication_failed_check_key, Toast.LENGTH_LONG, GB.WARN);
@ -244,7 +323,7 @@ public class CmfWatchProSupport extends AbstractBTLEDeviceSupport implements Cmf
characteristicCommandWrite.setSessionKey(sessionKey); characteristicCommandWrite.setSessionKey(sessionKey);
characteristicDataRead.setSessionKey(sessionKey); characteristicDataRead.setSessionKey(sessionKey);
characteristicDataWrite.setSessionKey(sessionKey); characteristicDataWrite.setSessionKey(sessionKey);
} catch (final GeneralSecurityException e) { } catch (final Exception e) {
LOG.error("Failed to compute session key from auth nonce", e); LOG.error("Failed to compute session key from auth nonce", e);
return; return;
} }
@ -363,23 +442,70 @@ public class CmfWatchProSupport extends AbstractBTLEDeviceSupport implements Cmf
builder.queue(getQueue()); builder.queue(getQueue());
} }
private static byte[] getSecretKey(final GBDevice device) { private void handleShellCommand(final byte[] bytes) {
final byte[] authKeyBytes = new byte[16]; final String shellCommand = new String(bytes).strip();
if (!shellCommand.startsWith("GETSECRET:")) {
LOG.error("Got unknown shell command: {}", GB.hexdump(bytes));
return;
}
if (!shellCommand.endsWith(",OK")) {
LOG.error("Failed to get secret: {}", GB.hexdump(bytes));
authNegotiationFailed();
return;
}
final byte[] signedRandom1;
try {
new Random().nextBytes(authRandom1);
final byte[] secretBytes = GB.hexStringToByteArray(shellCommand.substring(10, 10 + 32));
System.arraycopy(secretBytes, 0, authAppSecret, 0, authAppSecret.length);
final MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
sha256.update(authRandom1);
sha256.update(authAppSecret);
signedRandom1 = sha256.digest();
LOG.debug("authRandom1: {}", GB.hexdump(authRandom1));
LOG.debug("authAppSecret: {}", GB.hexdump(authAppSecret));
LOG.debug("signedRandom1: {}", GB.hexdump(signedRandom1));
} catch (final Exception e) {
LOG.error("Failed to generate signed random1", e);
authNegotiationFailed();
return;
}
sendCommand("auth send signed random1", CmfCommand.AUTH_PAIR_REQUEST, ArrayUtils.addAll(authRandom1, signedRandom1));
}
private void authNegotiationFailed() {
GB.toast(getContext(), R.string.authentication_failed_negotiation, Toast.LENGTH_LONG, GB.WARN);
final GBDevice device = getDevice();
if (device != null) {
GBApplication.deviceService(device).disconnect();
}
}
@Nullable
private static byte[] getSecretKey(final GBDevice device) {
final SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()); final SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress());
final String authKey = sharedPrefs.getString("authkey", "").trim(); final String authKey = sharedPrefs.getString("authkey", "").trim();
if (StringUtils.isNotBlank(authKey)) { if (StringUtils.isBlank(authKey)) {
final byte[] srcBytes; return null;
// 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));
} }
final byte[] authKeyBytes = new byte[16];
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; return authKeyBytes;
} }

View File

@ -1156,6 +1156,7 @@
<string name="authenticating">Authenticating</string> <string name="authenticating">Authenticating</string>
<string name="authentication_required">Authentication required</string> <string name="authentication_required">Authentication required</string>
<string name="authentication_failed_check_key">Authentication failed, please check auth key</string> <string name="authentication_failed_check_key">Authentication failed, please check auth key</string>
<string name="authentication_failed_negotiation">Authentication key negotiation failed</string>
<string name="activity_prefs_sleep_duration">Preferred sleep duration in hours</string> <string name="activity_prefs_sleep_duration">Preferred sleep duration in hours</string>
<string name="device_hw">Hardware revision: %1$s</string> <string name="device_hw">Hardware revision: %1$s</string>
<string name="device_fw">Firmware version: %1$s</string> <string name="device_fw">Firmware version: %1$s</string>