Skip to content

Commit

Permalink
feat(fw-updater): protect against hardware/firmware mismatches (and t…
Browse files Browse the repository at this point in the history
…herefore hard bricks) by analysing firmware files

Note: Compare with the SKU reported by the earbuds instead of BluetoothImpl.ActiveModel because users can spoof the device model during setup
  • Loading branch information
timschneeb committed Feb 16, 2024
1 parent 77f9d81 commit 8375c23
Show file tree
Hide file tree
Showing 21 changed files with 272 additions and 95 deletions.
2 changes: 1 addition & 1 deletion GalaxyBudsClient/GalaxyBudsClient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
<Platforms>AnyCPU</Platforms>
<DisableWinExeOutputInference>false</DisableWinExeOutputInference>
<ApplicationManifest>app.manifest</ApplicationManifest>
<LangVersion>9</LangVersion>
<LangVersion>11</LangVersion>
<ApplicationId>me.timschneeberger.galaxybudsclient</ApplicationId>

<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
Expand Down
1 change: 1 addition & 0 deletions GalaxyBudsClient/Interface/Dialogs/BudsPopup.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ public void UpdateSettings()

switch (BluetoothImpl.Instance.ActiveModel)
{
// TODO: put this in DeviceSpec
case Models.Buds:
break;
case Models.BudsPlus:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public sealed class ManualPairDialog : Window
{
private readonly IReadOnlyList<String> _modelCache
= Enum.GetValues(typeof(Models)).Cast<Models>().Where(x => x != Models.NULL)
.Select(x => x.GetDescription()).ToList();
.Select(x => x.GetModelMetadata()?.Name ?? string.Empty).ToList();

public IEnumerable ModelSource => _modelCache;

Expand Down
42 changes: 40 additions & 2 deletions GalaxyBudsClient/Interface/Pages/FirmwareSelectionPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
using GalaxyBudsClient.Interface.Dialogs;
using GalaxyBudsClient.Interface.Elements;
using GalaxyBudsClient.Interface.Items;
using GalaxyBudsClient.Message;
using GalaxyBudsClient.Model.Attributes;
using GalaxyBudsClient.Model.Firmware;
using GalaxyBudsClient.Model.Specifications;
using GalaxyBudsClient.Platform;
using GalaxyBudsClient.Utils;
using GalaxyBudsClient.Utils.DynamicLocalization;
Expand Down Expand Up @@ -89,7 +91,9 @@ private async void SelectFromDisk()
Title = Loc.Resolve("cact_notice"),
Description = Loc.Resolve("fw_select_external_note"),
}.ShowDialog<bool>(MainWindow.Instance);


_ = BluetoothImpl.Instance.SendRequestAsync(SPPMessage.MessageIds.DEBUG_SKU);

if (result)
{
OpenFileDialog dlg = new OpenFileDialog {Filters = new List<FileDialogFilter>()
Expand All @@ -110,6 +114,10 @@ private async void SelectFromDisk()

public override void OnPageShown()
{
// Make sure that we have the current hardware model cached, if supported
if(BluetoothImpl.Instance.DeviceSpec.Supports(IDeviceSpec.Feature.DebugSku))
_ = BluetoothImpl.Instance.SendRequestAsync(SPPMessage.MessageIds.DEBUG_SKU);

RefreshList();
}

Expand Down Expand Up @@ -202,9 +210,39 @@ private async Task PrepareInstallation(byte[] data, string buildName)
return;
}

/*
* Safety check: Verify whether the firmware binary is compatible with the current device to avoid hard bricks.
*
* We cannot rely on BluetoothImpl.Instance.ActiveModel here, because users can spoof the device model
* during setup using the "Advanced" menu for troubleshooting. If available, we use the SKU instead.
*/
var connectedModel = DeviceMessageCache.Instance.DebugSku?.ModelFromSku() ?? BluetoothImpl.Instance.ActiveModel;
var firmwareModel = binary.DetectModel();
if (firmwareModel == null)
{
Log.Warning("FirmwareSelectionPage.PrepareInstallation: Firmware model is null; skipping verification");
}
else if(connectedModel != firmwareModel)
{
await new MessageBox()
{
Title = Loc.Resolve("fw_select_verify_fail"),
Description = string.Format(
Loc.Resolve("fw_select_verify_model_mismatch_fail"),
firmwareModel.Value.GetModelMetadata()?.Name ?? Loc.Resolve("unknown"),
connectedModel.GetModelMetadata()?.Name
)
}.ShowDialog(MainWindow.Instance);
return;
}

var result = await new QuestionBox()
{
Title = string.Format(Loc.Resolve("fw_select_confirm"), binary.BuildName, BluetoothImpl.Instance.ActiveModel.GetDescription()),
Title = string.Format(
Loc.Resolve("fw_select_confirm"),
binary.BuildName,
BluetoothImpl.Instance.ActiveModel.GetModelMetadata()?.Name ?? Loc.Resolve("unknown")
),
Description = Loc.Resolve("fw_select_confirm_desc"),
MinWidth = 600,
MaxWidth = 600,
Expand Down
56 changes: 21 additions & 35 deletions GalaxyBudsClient/Message/Decoder/DebugGetAllDataParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ public override void ParseMessage(SPPMessage msg)
int hw2 = (msg.Payload[0] & 15);

HardwareVersion = "rev" + hw1.ToString("X") + "." + hw2.ToString("X");
SoftwareVersion = VersionDataToString(msg.Payload, 1, "R");
SoftwareVersion = VersionDataToString(msg.Payload, 1);
TouchSoftwareVersion = $"0x{msg.Payload[4]:X}";
LeftBluetoothAddress = BytesToMacString(msg.Payload, 5);
RightBluetoothAddress = BytesToMacString(msg.Payload, 11);
Expand Down Expand Up @@ -138,7 +138,7 @@ public override void ParseMessage(SPPMessage msg)
MsgVersion = msg.Payload[0];

HardwareVersion = "rev" + hw1.ToString("X") + "." + hw2.ToString("X");
SoftwareVersion = VersionDataToString(msg.Payload, 2, "R");
SoftwareVersion = VersionDataToString(msg.Payload, 2);
TouchSoftwareVersion = $"0x{msg.Payload[5]:X}";
LeftBluetoothAddress = BytesToMacString(msg.Payload, 6);
RightBluetoothAddress = BytesToMacString(msg.Payload, 12);
Expand Down Expand Up @@ -190,10 +190,11 @@ public override void ParseMessage(SPPMessage msg)
RightCradleBatt = msg.Payload[82];
}
}
private String BytesToMacString(byte[] payload, int startIndex)

private string BytesToMacString(IReadOnlyList<byte> payload, int startIndex)
{
StringBuilder sb = new StringBuilder();
for (int i13 = 0; i13 < 6; i13++)
var sb = new StringBuilder();
for (var i13 = 0; i13 < 6; i13++)
{
if (i13 != 0)
{
Expand All @@ -205,16 +206,18 @@ private String BytesToMacString(byte[] payload, int startIndex)
return sb.ToString();
}

private String VersionDataToString(byte[] payload, int startIndex, String side)
private string VersionDataToString(IReadOnlyList<byte> payload, int startIndex)
{
var buildPrefix = ActiveModel.GetModelMetadata()?.BuildPrefix ?? "R???";

if (ActiveModel == Models.Buds)
{
int swVarIndex = payload[startIndex];
int swYearIndex = (payload[startIndex + 1] & 240) >> 4;
int swMonthIndex = payload[startIndex + 1] & 15;
byte swRelVerIndex = payload[startIndex + 2];
var swYearIndex = (payload[startIndex + 1] & 240) >> 4;
var swMonthIndex = payload[startIndex + 1] & 15;
var swRelVerIndex = payload[startIndex + 2];

String swRelVarString;
string swRelVarString;
if (swRelVerIndex <= 15)
{
swRelVarString = (swRelVerIndex & 255).ToString("X");
Expand All @@ -224,37 +227,20 @@ private String VersionDataToString(byte[] payload, int startIndex, String side)
swRelVarString = _swRelVer[swRelVerIndex - 16];
}

return side + "170XX" + _swVer[swVarIndex] + "0A" + _swYear[swYearIndex] + _swMonth[swMonthIndex] +
return buildPrefix + "XX" + _swVer[swVarIndex] + "0A" + _swYear[swYearIndex] + _swMonth[swMonthIndex] +
swRelVarString;
}
else
{
String swVar = (payload[startIndex] & 1) == 0 ? "E" : "U";
int isFotaDm = (payload[startIndex] & 240) >> 4;
var swVar = (payload[startIndex] & 1) == 0 ? "E" : "U";
var isFotaDm = (payload[startIndex] & 240) >> 4;

int swYearIndex = (payload[startIndex + 1] & 240) >> 4;
int swMonthIndex = payload[startIndex + 1] & 15;
byte swRelVerIndex = payload[startIndex + 2];

string pre;
switch (ActiveModel)
{
case Models.BudsPlus:
pre = "175XX";
break;
case Models.BudsLive:
pre = "180XX";
break;
case Models.BudsPro:
pre = "190XX";
break;
default:
pre = "???XX";
break;
}
var swYearIndex = (payload[startIndex + 1] & 240) >> 4;
var swMonthIndex = payload[startIndex + 1] & 15;
var swRelVerIndex = payload[startIndex + 2];

return side + pre + swVar + "0A" + _swYear[swYearIndex] + _swMonth[swMonthIndex] +
_swRelVer[swRelVerIndex];
return buildPrefix + "XX" + swVar + "0A"
+ _swYear[swYearIndex] + _swMonth[swMonthIndex] + _swRelVer[swRelVerIndex];
}
}
}
Expand Down
46 changes: 35 additions & 11 deletions GalaxyBudsClient/Message/Decoder/DebugSkuParser.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,50 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using GalaxyBudsClient.Model.Attributes;
using GalaxyBudsClient.Model.Constants;

namespace GalaxyBudsClient.Message.Decoder
{
class DebugSkuParser : BaseMessageParser
public class DebugSkuParser : BaseMessageParser
{
public override SPPMessage.MessageIds HandledType => SPPMessage.MessageIds.DEBUG_SKU;

public char a { set; get; }
public char b { set; get; }
public char c { set; get; }
public char d { set; get; }
public string? LeftSku { set; get; }
public string? RightSku { set; get; }

public override void ParseMessage(SPPMessage msg)
{
if (msg.Id != HandledType)
return;

a = (char)msg.Payload[12];
b = (char)msg.Payload[13];
c = (char)msg.Payload[26];
d = (char)msg.Payload[27];
var payload = msg.Payload;
if (payload.Length >= 14)
{
LeftSku = Encoding.UTF8.GetString(payload, 0, 14);
}
if (payload.Length >= 14 * 2)
{
RightSku = Encoding.UTF8.GetString(payload, 14, 14);
}
}


public Models? ModelFromSku()
{
var build = LeftSku ?? RightSku;
if (build == null)
return null;

foreach (var model in Enum.GetValues<Models>())
{
var pattern = model.GetModelMetadata()?.FwPattern;
if(pattern == null)
continue;

if (build.Contains(pattern))
return model;
}
return null;
}
}
}
5 changes: 4 additions & 1 deletion GalaxyBudsClient/Message/DeviceMessageCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ public static void Init()
}
}

public DeviceMessageCache()
private DeviceMessageCache()
{
SPPMessageHandler.Instance.DebugSkuUpdate += (sender, parser) => DebugSku = parser;
SPPMessageHandler.Instance.ExtendedStatusUpdate += (sender, parser) => ExtendedStatusUpdate = parser;
SPPMessageHandler.Instance.StatusUpdate += (sender, parser) => StatusUpdate = parser;
SPPMessageHandler.Instance.GetAllDataResponse += (sender, parser) => DebugGetAllData = parser;
Expand All @@ -45,9 +46,11 @@ public void Clear()
DebugGetAllData = null;
ExtendedStatusUpdate = null;
StatusUpdate = null;
DebugSku = null;
}

public DebugGetAllDataParser? DebugGetAllData { set; get; }
public DebugSkuParser? DebugSku { set; get; }
public ExtendedStatusUpdateParser? ExtendedStatusUpdate { set; get; }
public StatusUpdateParser? StatusUpdate { set; get; }

Expand Down
1 change: 0 additions & 1 deletion GalaxyBudsClient/Message/FirmwareTransferManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,6 @@ public bool IsInProgress()

public async void Cancel()
{
_binary?.Dispose();
_binary = null;
_mtuSize = 0;
_currentSegment = 0;
Expand Down
4 changes: 4 additions & 0 deletions GalaxyBudsClient/Message/SPPMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public static SPPMessageHandler Instance
public event EventHandler<MuteUpdateParser>? FindMyGearMuteUpdate;
public event EventHandler? FindMyGearStopped;
public event EventHandler<FitTestParser>? FitTestResult;
public event EventHandler<DebugSkuParser>? DebugSkuUpdate;

public void MessageReceiver(object? sender, SPPMessage e)
{
Expand Down Expand Up @@ -116,6 +117,9 @@ public void DispatchEvent(BaseMessageParser? parser, SPPMessage.MessageIds? ids
NoiseControlUpdateResponse?.Invoke(this,
(parser as NoiseControlUpdateParser)?.Mode ?? NoiseControlMode.Off);
break;
case SPPMessage.MessageIds.DEBUG_SKU:
DebugSkuUpdate?.Invoke(this, (parser as DebugSkuParser)!);
break;
}
}

Expand Down
54 changes: 54 additions & 0 deletions GalaxyBudsClient/Model/Attributes/ModelMetadataAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Linq;

namespace GalaxyBudsClient.Model.Attributes
{
[AttributeUsage(AttributeTargets.Field)]
internal class ModelMetadataAttribute : Attribute
{
/**
* Friendly name, used for display purposes
*/
public required string Name { get; set; }
/**
* Used to detect corresponding device models for firmware update files
* Firmware archives do not contain model information in their headers,
* so we check whether the pattern matches the binary
*/
public required string FwPattern { get; set; }
/**
* Used to detect corresponding device models from build strings found in DEBUG_GET_ALL_DATA
*/
public required string BuildPrefix { get; set; }
}

// TODO: all of these Get<Attribute> methods are very similar, they could be refactored into a single method
internal static class ModelMetadataAttributeExtension
{
public static ModelMetadataAttribute? GetModelMetadata<T>(this T e) where T : IConvertible
{
if (e is not Enum)
return null;

var type = e.GetType();
foreach (var obj in Enum.GetValues(type))
{
if (obj == null || (int)obj != e.ToInt32(CultureInfo.InvariantCulture))
continue;

var memInfo = type.GetMember(type.GetEnumName((int) obj) ?? string.Empty);

if (memInfo[0]
.GetCustomAttributes(typeof(ModelMetadataAttribute), false)
.FirstOrDefault() is ModelMetadataAttribute attribute)
{
return attribute;
}
}

return null;
}
}
}
14 changes: 7 additions & 7 deletions GalaxyBudsClient/Model/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,19 +136,19 @@ public enum PopupPlacement
public enum Models
{
NULL = 0,
[Description("Galaxy Buds (2019)")]
[ModelMetadata(Name = "Galaxy Buds (2019)", FwPattern = "R170", BuildPrefix = "R170")]
Buds = 1,
[Description("Galaxy Buds+ (2020)")]
[ModelMetadata(Name = "Galaxy Buds+ (2020)", FwPattern = "SM-R175", BuildPrefix = "R175")]
BudsPlus = 2,
[Description("Galaxy Buds Live (2020)")]
[ModelMetadata(Name = "Galaxy Buds Live (2020)", FwPattern = "SM-R180", BuildPrefix = "R180")]
BudsLive = 3,
[Description("Galaxy Buds Pro (2021)")]
[ModelMetadata(Name = "Galaxy Buds Pro (2021)", FwPattern = "SM-R190", BuildPrefix = "R190")]
BudsPro = 4,
[Description("Galaxy Buds2 (2021)")]
[ModelMetadata(Name = "Galaxy Buds2 (2021)", FwPattern = "SM-R177", BuildPrefix = "R177")]
Buds2 = 5,
[Description("Galaxy Buds2 Pro (2022)")]
[ModelMetadata(Name = "Galaxy Buds2 Pro (2022)", FwPattern = "SM-R510", BuildPrefix = "R510")]
Buds2Pro = 6,
[Description("Galaxy Buds FE (2023)")]
[ModelMetadata(Name = "Galaxy Buds FE (2023)", FwPattern = "SM-R400N", BuildPrefix = "R400N")]
BudsFe = 7
}

Expand Down
Loading

0 comments on commit 8375c23

Please sign in to comment.