diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java index 4925fa241..a99953d9a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java @@ -2,13 +2,6 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fos import android.os.Build; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.zip.CRC32; - -import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationConfiguration; import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationHRConfiguration; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; @@ -16,19 +9,16 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSuppo import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.RequestMtuRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.SetDeviceStateRequest; -import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.configuration.ConfigurationPutRequest; -import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.NotificationFilterPutRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.PlayNotificationRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.authentication.VerifyPrivateKeyRequest; -import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.Image; -import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.image.ImagesPutRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.information.GetDeviceInformationRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification.NotificationFilterPutHRRequest; -import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification.NotificationImagePutRequest; -import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification.PlayNotificationHRRequest; -import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.PlayNotificationRequest; -import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.misfit.SetCurrentStepCountRequest; -import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.utils.StringUtils; public class FossilHRWatchAdapter extends FossilWatchAdapter { + private byte[] secretKey = new byte[]{(byte) 0x60, (byte) 0x26, (byte) 0xB7, (byte) 0xFD, (byte) 0xB2, (byte) 0x6D, (byte) 0x05, (byte) 0x5E, (byte) 0xDA, (byte) 0xF7, (byte) 0x4B, (byte) 0x49, (byte) 0x98, (byte) 0x78, (byte) 0x02, (byte) 0x38}; + private byte[] phoneRandomNumber; + private byte[] watchRandomNumber; + public FossilHRWatchAdapter(QHybridSupport deviceSupport) { super(deviceSupport); } @@ -40,11 +30,11 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter { } queueWrite(new VerifyPrivateKeyRequest( - new byte[]{(byte) 0x60, (byte) 0x26, (byte) 0xB7, (byte) 0xFD, (byte) 0xB2, (byte) 0x6D, (byte) 0x05, (byte) 0x5E, (byte) 0xDA, (byte) 0xF7, (byte) 0x4B, (byte) 0x49, (byte) 0x98, (byte) 0x78, (byte) 0x02, (byte) 0x38}, - getDeviceSupport().getQueue() + this.getSecretKey(), + this )); - try { + /*try { FileInputStream fis = new FileInputStream("/sdcard/Q/images/icWhatsapp.icon"); byte[] whatsappData = new byte[fis.available()]; fis.read(whatsappData); @@ -67,15 +57,18 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter { this)); } catch (IOException e) { e.printStackTrace(); - } + }*/ // icons queueWrite(new NotificationFilterPutHRRequest(new NotificationHRConfiguration[]{ - new NotificationHRConfiguration("twitter", -1), new NotificationHRConfiguration("com.whatsapp", -1), + new NotificationHRConfiguration("generic", -1), + // new NotificationHRConfiguration("twitter", -1), }, this)); - queueWrite(new nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.PlayNotificationRequest("com.whatsapp", "Test App", "this is a generic message", this)); - queueWrite(new nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.PlayNotificationRequest("twitter", "Twitter", "huehuehue", this)); + queueWrite(new PlayNotificationRequest("com.whatsapp", "WhatsAp", "wHATSaPP", this)); + queueWrite(new PlayNotificationRequest("twitter", "Twitter", "tWITTER", this)); + + queueWrite(new GetDeviceInformationRequest(this)); // syncConfiguration(); @@ -90,7 +83,31 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter { public boolean playRawNotification(NotificationSpec notificationSpec) { String sender = notificationSpec.sender; if(sender == null) sender = notificationSpec.sourceName; - queueWrite(new nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification.PlayNotificationRequest("generic", notificationSpec.sourceName, notificationSpec.body, this)); + queueWrite(new PlayNotificationRequest("generic", notificationSpec.sourceName, notificationSpec.body, this)); return true; } + + public byte[] getSecretKey() { + return secretKey; + } + + public void setSecretKey(byte[] secretKey) { + this.secretKey = secretKey; + } + + public void setPhoneRandomNumber(byte[] phoneRandomNumber) { + this.phoneRandomNumber = phoneRandomNumber; + } + + public byte[] getPhoneRandomNumber() { + return phoneRandomNumber; + } + + public void setWatchRandomNumber(byte[] watchRandomNumber) { + this.watchRandomNumber = watchRandomNumber; + } + + public byte[] getWatchRandomNumber() { + return watchRandomNumber; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil/notification/PlayNotificationRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil/notification/PlayNotificationRequest.java index c8e6f4c15..926581fae 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil/notification/PlayNotificationRequest.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil/notification/PlayNotificationRequest.java @@ -48,9 +48,10 @@ public class PlayNotificationRequest extends FilePutRequest { byte flags = getFlags(); byte uidLength = (byte) 4; byte appBundleCRCLength = (byte) 4; - String nullTerminatedTitle = StringUtils.terminateNull(title); Charset charsetUTF8 = Charset.forName("UTF-8"); + + String nullTerminatedTitle = StringUtils.terminateNull(title); byte[] titleBytes = nullTerminatedTitle.getBytes(charsetUTF8); String nullTerminatedSender = StringUtils.terminateNull(sender); byte[] senderBytes = nullTerminatedSender.getBytes(charsetUTF8); @@ -77,7 +78,7 @@ public class PlayNotificationRequest extends FilePutRequest { lengthBuffer = ByteBuffer.allocate(mainBufferLength - lengthBufferLength); lengthBuffer.order(ByteOrder.LITTLE_ENDIAN); - lengthBuffer.putInt(0); + lengthBuffer.putInt(10); // messageId lengthBuffer.putInt(packageCrc); lengthBuffer.put(titleBytes); lengthBuffer.put(senderBytes); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/authentication/VerifyPrivateKeyRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/authentication/VerifyPrivateKeyRequest.java index 9c0f8a51e..ed9a1f0cc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/authentication/VerifyPrivateKeyRequest.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/authentication/VerifyPrivateKeyRequest.java @@ -16,18 +16,20 @@ import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; -import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEQueue; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil_hr.FossilHRWatchAdapter; import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest; public class VerifyPrivateKeyRequest extends FossilRequest { - private final BtLEQueue queue; - private byte[] key; + private final FossilHRWatchAdapter adapter; + private byte[] key, randomPhoneNumber; private boolean isFinished = false; - public VerifyPrivateKeyRequest(byte[] key, BtLEQueue queue) { - this.queue = queue; + public VerifyPrivateKeyRequest(byte[] key, FossilHRWatchAdapter adapter) { + this.adapter = adapter; this.key = key; + + adapter.setPhoneRandomNumber(randomPhoneNumber); } @Override @@ -56,6 +58,11 @@ public class VerifyPrivateKeyRequest extends FossilRequest { System.arraycopy(result, 0, bytesToEncrypt, 8, 8); System.arraycopy(result, 8, bytesToEncrypt, 0, 8); + byte[] watchRandomNumber = new byte[8]; + System.arraycopy(result, 0, watchRandomNumber, 0, 8); + + adapter.setWatchRandomNumber(watchRandomNumber); + cipher = Cipher.getInstance("AES/CBC/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})); result = cipher.doFinal(bytesToEncrypt); @@ -69,7 +76,7 @@ public class VerifyPrivateKeyRequest extends FossilRequest { new TransactionBuilder("send encrypted random numbers") .write(characteristic, payload) - .queue(this.queue); + .queue(this.adapter.getDeviceSupport().getQueue()); } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException e) { throw new RuntimeException(e); } @@ -93,11 +100,11 @@ public class VerifyPrivateKeyRequest extends FossilRequest { buffer.put((byte) 0x01); buffer.put((byte) 0x01); - byte[] random = new byte[8]; + this.randomPhoneNumber = new byte[8]; - new Random().nextBytes(random); + new Random().nextBytes(randomPhoneNumber); - buffer.put(random); + buffer.put(randomPhoneNumber); return buffer.array(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/file/FileEncryptedGetRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/file/FileEncryptedGetRequest.java new file mode 100644 index 000000000..fd95d8013 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/file/FileEncryptedGetRequest.java @@ -0,0 +1,165 @@ +/* Copyright (C) 2019 Daniel Dakhno + + 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.qhybrid.requests.fossil_hr.file; + +import android.bluetooth.BluetoothGattCharacteristic; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.UUID; +import java.util.zip.CRC32; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.QHybridSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil_hr.FossilHRWatchAdapter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.Request; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest; + +public abstract class FileEncryptedGetRequest extends FossilRequest { + private short handle; + private FossilHRWatchAdapter adapter; + + private ByteBuffer fileBuffer; + + private byte[] fileData; + + private boolean finished = false; + + public FileEncryptedGetRequest(short handle, FossilHRWatchAdapter adapter) { + this.handle = handle; + this.adapter = adapter; + + this.data = + createBuffer() + .putShort(handle) + .putInt(0) + .putInt(0xFFFFFFFF) + .array(); + } + + public FossilWatchAdapter getAdapter() { + return adapter; + } + + @Override + public boolean isFinished(){ + return finished; + } + + @Override + public void handleResponse(BluetoothGattCharacteristic characteristic) { + byte[] value = characteristic.getValue(); + byte first = value[0]; + if(characteristic.getUuid().toString().equals("3dda0003-957f-7d4a-34a6-74696673696d")){ + if((first & 0x0F) == 1){ + ByteBuffer buffer = ByteBuffer.wrap(value); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + short handle = buffer.getShort(1); + int size = buffer.getInt(4); + + byte status = buffer.get(3); + + if(status != 0){ + throw new RuntimeException("FileGet error: " + status); + } + + if(this.handle != handle){ + throw new RuntimeException("handle: " + handle + " expected: " + this.handle); + } + log("file size: " + size); + fileBuffer = ByteBuffer.allocate(size); + }else if((first & 0x0F) == 8){ + this.finished = true; + + ByteBuffer buffer = ByteBuffer.wrap(value); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + short handle = buffer.getShort(1); + if(this.handle != handle){ + throw new RuntimeException("handle: " + handle + " expected: " + this.handle); + } + + CRC32 crc = new CRC32(); + crc.update(this.fileData); + + int crcExpected = buffer.getInt(8); + + if((int) crc.getValue() != crcExpected){ + throw new RuntimeException("handle: " + handle + " expected: " + this.handle); + } + + this.handleFileData(this.fileData); + } + }else if(characteristic.getUuid().toString().equals("3dda0004-957f-7d4a-34a6-74696673696d")){ + SecretKeySpec keySpec = new SecretKeySpec(this.adapter.getSecretKey(), "AES"); + try { + Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); + + byte[] fileIV = new byte[16]; + + + byte[] phoneRandomNumber = adapter.getPhoneRandomNumber(); + byte[] watchRandomNumber = adapter.getWatchRandomNumber(); + + System.arraycopy(phoneRandomNumber, 0, fileIV, 2, 6); + System.arraycopy(watchRandomNumber, 0, fileIV, 9, 7); + + fileIV[7]++; + + cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(fileIV)); + + byte[] result = cipher.doFinal(value); + + fileBuffer.put(result, 1, result.length - 1); + if((result[0] & 0x80) == 0x80){ + this.fileData = fileBuffer.array(); + } + } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public UUID getRequestUUID() { + return UUID.fromString("3dda0003-957f-7d4a-34a6-74696673696d"); + } + + @Override + public byte[] getStartSequence() { + return new byte[]{1}; + } + + @Override + public int getPayloadLength() { + return 11; + } + + abstract public void handleFileData(byte[] fileData); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/file/FileEncryptedLookupAndGetRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/file/FileEncryptedLookupAndGetRequest.java new file mode 100644 index 000000000..a5d64d988 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/file/FileEncryptedLookupAndGetRequest.java @@ -0,0 +1,40 @@ +/* Copyright (C) 2019 Daniel Dakhno + + 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.qhybrid.requests.fossil_hr.file; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil_hr.FossilHRWatchAdapter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileGetRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FileLookupRequest; + +public abstract class FileEncryptedLookupAndGetRequest extends FileLookupRequest { + public FileEncryptedLookupAndGetRequest(byte fileType, FossilHRWatchAdapter adapter) { + super(fileType, adapter); + } + + @Override + public void handleFileLookup(short fileHandle){ + getAdapter().queueWrite(new FileEncryptedGetRequest(getHandle(), (FossilHRWatchAdapter) getAdapter()) { + @Override + public void handleFileData(byte[] fileData) { + FileEncryptedLookupAndGetRequest.this.handleFileData(fileData); + } + }, true); + } + + abstract public void handleFileData(byte[] fileData); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/information/GetDeviceInformationRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/information/GetDeviceInformationRequest.java new file mode 100644 index 000000000..a780a1ec6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/requests/fossil_hr/information/GetDeviceInformationRequest.java @@ -0,0 +1,18 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.information; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil.FossilWatchAdapter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil_hr.FossilHRWatchAdapter; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.FileEncryptedGetRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.file.FileEncryptedLookupAndGetRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.utils.StringUtils; + +public class GetDeviceInformationRequest extends FileEncryptedLookupAndGetRequest { + public GetDeviceInformationRequest(FossilHRWatchAdapter adapter) { + super((byte) 0x08, adapter); + } + + @Override + public void handleFileData(byte[] fileData) { + log("device info: " + StringUtils.bytesToHex(fileData)); + } +}