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));
+ }
+}