From 7fa5cd1be56247785dbcf0727fe48eb59b8df84b Mon Sep 17 00:00:00 2001 From: Daniele Gobbetti Date: Wed, 14 Aug 2024 10:04:32 +0200 Subject: [PATCH] Garmin: further work for auth negotiation Add status message parsing and change the reply logic for watch-initiated Auth (in attempt to fix #3986): before this changeset the phone would reply with a generic ACK and then send a request to the watch for setting the auth (with all zeroes); after this changeset the phone replies with a specific auth ack/status message but it ignores what the watch requested and acknowledges back all zeroes. Blindly implemented based on the legacy vivomoveHR code, not tested against real devices. --- .../messages/AuthNegotiationMessage.java | 8 +- .../status/AuthNegotiationStatusMessage.java | 83 +++++++++++++++++++ .../messages/status/GFDIStatusMessage.java | 2 + 3 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/AuthNegotiationStatusMessage.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/AuthNegotiationMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/AuthNegotiationMessage.java index 8b307b94d..fb79f50e8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/AuthNegotiationMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/AuthNegotiationMessage.java @@ -4,6 +4,8 @@ import org.apache.commons.lang3.EnumUtils; import java.util.EnumSet; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.AuthNegotiationStatusMessage; + public class AuthNegotiationMessage extends GFDIMessage { private final int unknown; @@ -16,7 +18,7 @@ public class AuthNegotiationMessage extends GFDIMessage { LOG.info("Message {}, unkByte: {}, flags: {}", garminMessage, unknown, requestedAuthFlags); - this.statusMessage = getStatusMessage(); + this.statusMessage = new AuthNegotiationStatusMessage(garminMessage, Status.ACK, AuthNegotiationStatusMessage.AuthNegotiationStatus.GUESS_OK, 0, EnumSet.noneOf(AuthFlags.class)); } public static AuthNegotiationMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) { @@ -38,10 +40,10 @@ public class AuthNegotiationMessage extends GFDIMessage { writer.writeByte(0); writer.writeInt((int) EnumUtils.generateBitVector(AuthFlags.class, EnumSet.noneOf(AuthFlags.class))); - return true; + return false; } - private enum AuthFlags { + public enum AuthFlags { UNK_00000001, //saw in logs UNK_00000010, UNK_00000100, diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/AuthNegotiationStatusMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/AuthNegotiationStatusMessage.java new file mode 100644 index 000000000..cc4bf474e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/AuthNegotiationStatusMessage.java @@ -0,0 +1,83 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status; + +import androidx.annotation.Nullable; + +import org.apache.commons.lang3.EnumUtils; + +import java.util.EnumSet; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.AuthNegotiationMessage; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter; + +public class AuthNegotiationStatusMessage extends GFDIStatusMessage { + + private final Status status; + private final AuthNegotiationStatus authNegotiationStatus; + private final int unknown; + private final EnumSet authFlags; + private final boolean sendOutgoing; + + public AuthNegotiationStatusMessage(GarminMessage garminMessage, Status status, AuthNegotiationStatus authNegotiationStatus, int unknown, EnumSet authFlags) { + this(garminMessage, status, authNegotiationStatus, unknown, authFlags, true); + } + + public AuthNegotiationStatusMessage(GarminMessage garminMessage, Status status, AuthNegotiationStatus authNegotiationStatus, int unknown, EnumSet authFlags, boolean sendOutgoing) { + this.garminMessage = garminMessage; + this.status = status; + this.authNegotiationStatus = authNegotiationStatus; + this.unknown = unknown; + this.authFlags = authFlags; + this.sendOutgoing = sendOutgoing; + } + + public static AuthNegotiationStatusMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) { + final Status status = Status.fromCode(reader.readByte()); + final int authNegotiationStatusCode = reader.readByte(); + final AuthNegotiationStatus authNegotiationStatus = AuthNegotiationStatus.fromCode(authNegotiationStatusCode); + if (null == authNegotiationStatus) { + LOG.warn("Unknown auth negotiation status code {}", authNegotiationStatusCode); + return null; + } + final int unk = reader.readByte(); + final EnumSet authFlags = AuthNegotiationMessage.AuthFlags.fromBitMask(reader.readInt()); + + switch (authNegotiationStatus) { + case GUESS_OK: + LOG.info("Received {}/{} for message {} unkByte: {}, flags: {}", status, authNegotiationStatus, garminMessage, unk, authFlags); + break; + default: + LOG.warn("Received {}/{} for message {} unkByte: {}, flags: {}", status, authNegotiationStatus, garminMessage, unk, authFlags); + } + return new AuthNegotiationStatusMessage(garminMessage, status, authNegotiationStatus, unk, authFlags, false); + } + + @Override + protected boolean generateOutgoing() { + final MessageWriter writer = new MessageWriter(response); + writer.writeShort(0); // packet size will be filled below + writer.writeShort(GarminMessage.RESPONSE.getId()); + writer.writeShort(garminMessage.getId()); + writer.writeByte(status.ordinal()); + writer.writeByte(authNegotiationStatus.ordinal()); + writer.writeByte(unknown); + writer.writeInt((int) EnumUtils.generateBitVector(AuthNegotiationMessage.AuthFlags.class, authFlags)); + + return sendOutgoing; + } + + public enum AuthNegotiationStatus { + GUESS_OK, + GUESS_KO, + ; + + @Nullable + public static AuthNegotiationStatus fromCode(final int code) { + for (final AuthNegotiationStatus authNegotiationStatus : AuthNegotiationStatus.values()) { + if (authNegotiationStatus.ordinal() == code) { + return authNegotiationStatus; + } + } + return null; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/GFDIStatusMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/GFDIStatusMessage.java index 4276cd2ab..fc39b76c9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/GFDIStatusMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/GFDIStatusMessage.java @@ -32,6 +32,8 @@ public abstract class GFDIStatusMessage extends GFDIMessage { return FitDefinitionStatusMessage.parseIncoming(reader, originalGarminMessage); } else if (GarminMessage.FIT_DATA.equals(originalGarminMessage)) { return FitDataStatusMessage.parseIncoming(reader, originalGarminMessage); + } else if (GarminMessage.AUTH_NEGOTIATION.equals(originalGarminMessage)) { + return AuthNegotiationStatusMessage.parseIncoming(reader, originalGarminMessage); } else { final Status status = Status.fromCode(reader.readByte());