Removed key extraction code.

Moved command implementations to base class
Removed references to config file structure
Turned brand into an Enum
(Note: this may change so urls can be modified in app.config)
Base class virtualizes key encoding
Implemented a client that uses external token signing when dev doesn't have keys
Fixed diagnostics request
This commit is contained in:
Anonymous
2019-08-18 21:05:22 -04:00
parent 938e343fcf
commit 83fab5ffcf
14 changed files with 427 additions and 445 deletions

100
GM.Api/Brands.cs Normal file
View File

@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace GM.Api
{
public enum Brand
{
Opel,
Vauxhall,
Chevrolet,
OnStar,
Cadillac,
Buick,
Gmc
}
public static class BrandHelpers
{
public static string GetDisplayName(this Brand brand)
{
return brand.ToString();
}
public static string GetName(this Brand brand)
{
switch (brand)
{
case Brand.Opel:
return "opel";
case Brand.Vauxhall:
return "vauxhall";
case Brand.Chevrolet:
return "chevrolet";
case Brand.OnStar:
return "onstar";
case Brand.Cadillac:
return "cadillac";
case Brand.Buick:
return "buick";
case Brand.Gmc:
return "gmc";
default:
throw new InvalidOperationException("Unknown Brand");
}
}
public static string GetUrl(this Brand brand)
{
switch (brand)
{
case Brand.Opel:
return "https://api.eur.onstar.com/api";
case Brand.Vauxhall:
return "https://api.eur.onstar.com/api";
case Brand.Chevrolet:
return "https://api.gm.com/api";
case Brand.OnStar:
return "https://api.gm.com/api";
case Brand.Cadillac:
return "https://api.gm.com/api";
case Brand.Buick:
return "https://api.gm.com/api";
case Brand.Gmc:
return "https://api.gm.com/api";
default:
throw new InvalidOperationException("Unknown Brand");
}
}
public static Brand GetBrand(string brandName)
{
var cleanName = brandName.ToLowerInvariant();
switch (cleanName)
{
case "opel":
return Brand.Opel;
case "vauxhall":
return Brand.Vauxhall;
case "chevrolet":
return Brand.Chevrolet;
case "onstar":
return Brand.OnStar;
case "cadillac":
return Brand.Cadillac;
case "buick":
return Brand.Buick;
case "gmc":
return Brand.Gmc;
default:
throw new InvalidOperationException("Unknown Brand");
}
}
}
}

30
GM.Api/GMClient.cs Normal file
View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using GM.Api.Tokens;
namespace GM.Api
{
/// <summary>
/// GM Client implementation to be used when you have local access to the Client ID and Client Secret
/// </summary>
public class GMClient : GMClientBase
{
string _clientId;
JwtTool _jwtTool;
public GMClient(string deviceId, Brand brand, string clientId, string clientSecret) : base(deviceId, brand)
{
_clientId = clientId;
_jwtTool = new JwtTool(clientSecret);
}
protected override async Task<string> EncodeLoginRequest(LoginRequest request)
{
request.ClientId = _clientId;
return _jwtTool.EncodeToken(request);
}
}
}

View File

@@ -21,12 +21,9 @@ namespace GM.Api
public static int RetryCount { get; set; } = 3;
//TODO: consistent exception throwing
string _clientId;
string _deviceId;
JwtTool _jwtTool;
string _apiUrl;
string _host;
protected Brand _brand;
protected string _deviceId;
protected string _apiUrl;
HttpClient _client;
@@ -62,24 +59,20 @@ namespace GM.Api
/// <summary>
/// Create a new GMClient
/// </summary>
/// <param name="clientId">Client ID for authentication</param>
/// <param name="deviceId">Device ID (should be in the format of a GUID)</param>
/// <param name="clientSecret">Client Secret for authentication</param>
/// <param name="apiUrl">Base url for the API. Usually https://api.gm.com/api </param>
public GMClientBase(string clientId, string deviceId, string clientSecret, string apiUrl)
/// <param name="brand">One of the supported brands from </param>
public GMClientBase(string deviceId, Brand brand)
{
Setup(clientId, deviceId, clientSecret, apiUrl);
Setup(deviceId, brand);
}
void Setup(string clientId, string deviceId, string clientSecret, string apiUrl)
void Setup(string deviceId, Brand brand)
{
_clientId = clientId;
_brand = brand;
_deviceId = deviceId;
_jwtTool = new JwtTool(clientSecret);
_apiUrl = apiUrl;
_apiUrl = brand.GetUrl();
var uri = new Uri(_apiUrl);
_host = uri.Host;
_client = CreateClient(_host);
_client = CreateClient(uri.Host);
}
@@ -98,6 +91,19 @@ namespace GM.Api
}
protected abstract Task<string> EncodeLoginRequest(LoginRequest request);
LoginData DecodeLoginData(string token)
{
IJsonSerializer serializer = new SortedJsonSerializer();
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IDateTimeProvider dateTimeProvider = new UtcDateTimeProvider();
IJwtValidator validator = new JwtValidator(serializer, dateTimeProvider);
var decoder = new JwtDecoder(serializer, validator, urlEncoder);
return decoder.DecodeToObject<LoginData>(token);
}
#region Client Helpers
/// <summary>
@@ -157,6 +163,9 @@ namespace GM.Api
}
else if (resp.StatusCode == System.Net.HttpStatusCode.BadGateway || resp.StatusCode == System.Net.HttpStatusCode.Conflict || resp.StatusCode == System.Net.HttpStatusCode.GatewayTimeout || resp.StatusCode == System.Net.HttpStatusCode.InternalServerError || resp.StatusCode == System.Net.HttpStatusCode.RequestTimeout || resp.StatusCode == System.Net.HttpStatusCode.ResetContent || resp.StatusCode == System.Net.HttpStatusCode.ServiceUnavailable)
{
var respMessage = (await resp.Content.ReadAsStringAsync()) ?? "";
int f = 5;
//possible transient errors
//todo: log this
await Task.Delay(500);
@@ -226,7 +235,7 @@ namespace GM.Api
{
var payload = new LoginRequest()
{
ClientId = _clientId,
//ClientId = _clientId,
DeviceId = _deviceId,
Credential = onStarPin,
CredentialType = "PIN",
@@ -234,7 +243,8 @@ namespace GM.Api
Timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK")
};
var token = _jwtTool.EncodeToken(payload);
//var token = _jwtTool.EncodeToken(payload);
var token = await EncodeLoginRequest(payload);
using (var response = await PostAsync($"{_apiUrl}/v1/oauth/token/upgrade", new StringContent(token, Encoding.UTF8, "text/plain")))
{
@@ -264,7 +274,7 @@ namespace GM.Api
{
var payload = new LoginRequest()
{
ClientId = _clientId,
//ClientId = _clientId,
DeviceId = _deviceId,
GrantType = "password",
Nonce = helpers.GenerateNonce(),
@@ -274,7 +284,8 @@ namespace GM.Api
Username = username
};
var token = _jwtTool.EncodeToken(payload);
//var token = _jwtTool.EncodeToken(payload);
var token = await EncodeLoginRequest(payload);
using (var response = await PostAsync($"{_apiUrl}/v1/oauth/token", new StringContent(token, Encoding.UTF8, "text/plain"), true))
{
@@ -294,7 +305,8 @@ namespace GM.Api
return false;
}
var loginTokenData = _jwtTool.DecodeTokenToObject<LoginData>(rawResponseToken);
//var loginTokenData = _jwtTool.DecodeTokenToObject<LoginData>(rawResponseToken);
var loginTokenData = DecodeLoginData(rawResponseToken);
LoginData = loginTokenData;
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", LoginData.AccessToken);
@@ -315,7 +327,6 @@ namespace GM.Api
var payload = new LoginRequest()
{
ClientId = _clientId,
DeviceId = _deviceId,
GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer",
Nonce = helpers.GenerateNonce(),
@@ -324,7 +335,7 @@ namespace GM.Api
Assertion = LoginData.IdToken
};
var token = _jwtTool.EncodeToken(payload);
var token = await EncodeLoginRequest(payload);
using (var response = await PostAsync($"{_apiUrl}/v1/oauth/token", new StringContent(token, Encoding.UTF8, "text/plain"), true))
{
@@ -358,7 +369,7 @@ namespace GM.Api
}*/
// Not sure if the scope needs to be updated, as msso has been removed in the refresh request
var refreshData = _jwtTool.DecodeTokenToObject<LoginData>(rawResponseToken);
var refreshData = DecodeLoginData(rawResponseToken);
LoginData.AccessToken = refreshData.AccessToken;
LoginData.IssuedAtUtc = refreshData.IssuedAtUtc;
@@ -544,5 +555,173 @@ namespace GM.Api
#endregion
#region Command Implementations
/// <summary>
/// Retrieve Diagnostic data for the active vehicle
/// </summary>
/// <returns></returns>
public async Task<DiagnosticResponse[]> GetDiagnostics()
{
var cmdInfo = ActiveVehicle.GetCommand("diagnostics");
var reqObj = new JObject()
{
["diagnosticsRequest"] = new JObject()
{
["diagnosticItem"] = new JArray(cmdInfo.CommandData.SupportedDiagnostics.SupportedDiagnostic)
}
};
var result = await InitiateCommandAndWait("diagnostics", reqObj);
if (result == null) return null;
if ("success".Equals(result.Status, StringComparison.OrdinalIgnoreCase))
{
return result.Body.DiagnosticResponse;
}
else
{
return null;
}
}
/// <summary>
/// Issue an arbitrary command
/// </summary>
/// <param name="commandName">Name of the command. Must exists in the vehicle's configuration</param>
/// <param name="parameters">JSON parameters for the command</param>
/// <returns></returns>
public async Task<CommandResponse> IssueCommand(string commandName, JObject parameters = null)
{
return await InitiateCommandAndWait(commandName, parameters);
}
/// <summary>
/// Lock the active vehicles's doors and wait for completion
/// Privileged Command
/// </summary>
/// <returns>True or false for success</returns>
public async Task<bool> LockDoor()
{
var reqObj = new JObject()
{
["lockDoorRequest"] = new JObject()
{
["delay"] = 0
}
};
return await InitiateCommandAndWaitForSuccess("lockDoor", reqObj);
}
/// <summary>
/// Fails when the hotspot is off...
/// Note: the app uses diagnotics that also fail when the hotpot is off
/// </summary>
/// <returns></returns>
public async Task<HotspotInfo> GetHotspotInfo()
{
var resp = await InitiateCommandAndWait("getHotspotInfo", null);
return resp.Body.HotspotInfo;
}
/// <summary>
/// Send a turn-by-turn destination to the vehicle
/// Requires both coordinates and address info
/// Vehicle may not respond if turned off or may take a very long time to respond
/// </summary>
/// <param name="destination"></param>
/// <returns></returns>
public async Task<bool> SendTBTRoute(TbtDestination destination)
{
var reqObj = new JObject()
{
["tbtDestination"] = new JObject(destination)
};
return await InitiateCommandAndWaitForSuccess("sendTBTRoute", reqObj);
}
/// <summary>
/// Unlock the active vehicles's doors and wait for completion
/// Privileged Command
/// </summary>
/// <returns>True or false for success</returns>
public async Task<bool> UnlockDoor()
{
var reqObj = new JObject()
{
["unlockDoorRequest"] = new JObject()
{
["delay"] = 0
}
};
return await InitiateCommandAndWaitForSuccess("unlockDoor", reqObj);
}
/// <summary>
/// Remote start the active vehicle and wait for completion
/// Privileged Command
/// </summary>
/// <returns>True or false for success</returns>
public async Task<bool> Start()
{
return await InitiateCommandAndWaitForSuccess("start", null);
}
/// <summary>
/// Remote stop the active vehicle and wait for completion
/// Privileged Command
/// </summary>
/// <returns>True or false for success</returns>
public async Task<bool> CancelStart()
{
return await InitiateCommandAndWaitForSuccess("cancelStart", null);
}
/// <summary>
/// Set off remote alarm on the active vehicle and wait for completion
/// Privileged Command
/// </summary>
/// <returns>True or false for success</returns>
public async Task<bool> Alert()
{
var reqObj = new JObject()
{
["alertRequest"] = new JObject()
{
["action"] = new JArray() { "Honk", "Flash" },
["delay"] = 0,
["duration"] = 1,
["override"] = new JArray() { "DoorOpen", "IgnitionOn" }
}
};
return await InitiateCommandAndWaitForSuccess("alert", reqObj);
}
/// <summary>
/// Stop remote alarm on the active vehicle and wait for completion
/// Privileged Command
/// </summary>
/// <returns>True or false for success</returns>
public async Task<bool> CancelAlert()
{
return await InitiateCommandAndWaitForSuccess("cancelAlert", null);
}
#endregion
}
}

45
GM.Api/GMClientNoKey.cs Normal file
View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using GM.Api.Tokens;
namespace GM.Api
{
/// <summary>
/// GM Client implementation that uses a web service to sign the JWT tokens required for authentication
/// Use this if you do not have access to the Client ID and Client Secret
/// </summary>
public class GMClientNoKey : GMClientBase
{
string _tokenSignUrl;
HttpClient _tokenClient = new HttpClient();
/// <summary>
/// Create new GM Client
/// </summary>
/// <param name="deviceId">deviceId = string representation of a GUID</param>
/// <param name="brand">API is segmented by brand</param>
/// <param name="tokenSignUrl">URL for webservice that will sign JWT tokens (e.g. "https://gmsigner.herokuapp.com/")</param>
public GMClientNoKey(string deviceId, Brand brand, string tokenSignUrl) : base(deviceId, brand)
{
_tokenSignUrl = tokenSignUrl;
}
protected override async Task<string> EncodeLoginRequest(LoginRequest request)
{
var resp = await _tokenClient.PostAsJsonAsync($"{_tokenSignUrl}?brand={_brand.GetName()}", request);
if (resp.IsSuccessStatusCode)
{
return await resp.Content.ReadAsStringAsync();
}
else
{
string errorText = await resp.Content.ReadAsStringAsync();
throw new InvalidOperationException("Token sign failure: " + errorText);
}
}
}
}

View File

@@ -1,182 +0,0 @@
using GM.Api.Models;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace GM.Api
{
/// <summary>
/// Generic implementation of GM Client supporting a limited set of commands
/// </summary>
public class GenericGMClient : GMClientBase
{
public GenericGMClient(string clientId, string deviceId, string clientSecret, string apiUrl) : base(clientId, deviceId, clientSecret, apiUrl)
{
}
/// <summary>
/// Retrieve Diagnostic data for the active vehicle
/// </summary>
/// <returns></returns>
public async Task<DiagnosticResponse[]> GetDiagnostics()
{
var cmdInfo = ActiveVehicle.GetCommand("diagnostics");
var reqObj = new JObject()
{
["diagnosticItem"] = new JArray(cmdInfo.CommandData.SupportedDiagnostics.SupportedDiagnostic)
};
var result = await InitiateCommandAndWait("diagnostics", reqObj);
if (result == null) return null;
if ("success".Equals(result.Status, StringComparison.OrdinalIgnoreCase))
{
return result.Body.DiagnosticResponse;
}
else
{
return null;
}
}
/// <summary>
/// Issue an arbitrary command
/// </summary>
/// <param name="commandName">Name of the command. Must exists in the vehicle's configuration</param>
/// <param name="parameters">JSON parameters for the command</param>
/// <returns></returns>
public async Task<CommandResponse> IssueCommand(string commandName, JObject parameters = null)
{
return await InitiateCommandAndWait(commandName, parameters);
}
/// <summary>
/// Lock the active vehicles's doors and wait for completion
/// Privileged Command
/// </summary>
/// <returns>True or false for success</returns>
public async Task<bool> LockDoor()
{
var reqObj = new JObject()
{
["lockDoorRequest"] = new JObject()
{
["delay"] = 0
}
};
return await InitiateCommandAndWaitForSuccess("lockDoor", reqObj);
}
/// <summary>
/// Fails when the hotspot is off...
/// Note: the app uses diagnotics that also fail when the hotpot is off
/// </summary>
/// <returns></returns>
public async Task<HotspotInfo> GetHotspotInfo()
{
var resp = await InitiateCommandAndWait("getHotspotInfo", null);
return resp.Body.HotspotInfo;
}
/// <summary>
/// Send a turn-by-turn destination to the vehicle
/// Requires both coordinates and address info
/// Vehicle may not respond if turned off or may take a very long time to respond
/// </summary>
/// <param name="destination"></param>
/// <returns></returns>
public async Task<bool> SendTBTRoute(TbtDestination destination)
{
var reqObj = new JObject()
{
["tbtDestination"] = new JObject(destination)
};
return await InitiateCommandAndWaitForSuccess("sendTBTRoute", reqObj);
}
/// <summary>
/// Unlock the active vehicles's doors and wait for completion
/// Privileged Command
/// </summary>
/// <returns>True or false for success</returns>
public async Task<bool> UnlockDoor()
{
var reqObj = new JObject()
{
["unlockDoorRequest"] = new JObject()
{
["delay"] = 0
}
};
return await InitiateCommandAndWaitForSuccess("unlockDoor", reqObj);
}
/// <summary>
/// Remote start the active vehicle and wait for completion
/// Privileged Command
/// </summary>
/// <returns>True or false for success</returns>
public async Task<bool> Start()
{
return await InitiateCommandAndWaitForSuccess("start", null);
}
/// <summary>
/// Remote stop the active vehicle and wait for completion
/// Privileged Command
/// </summary>
/// <returns>True or false for success</returns>
public async Task<bool> CancelStart()
{
return await InitiateCommandAndWaitForSuccess("cancelStart", null);
}
/// <summary>
/// Set off remote alarm on the active vehicle and wait for completion
/// Privileged Command
/// </summary>
/// <returns>True or false for success</returns>
public async Task<bool> Alert()
{
var reqObj = new JObject()
{
["alertRequest"] = new JObject()
{
["action"] = new JArray() { "Honk", "Flash" },
["delay"] = 0,
["duration"] = 1,
["override"] = new JArray() { "DoorOpen", "IgnitionOn" }
}
};
return await InitiateCommandAndWaitForSuccess("alert", reqObj);
}
/// <summary>
/// Stop remote alarm on the active vehicle and wait for completion
/// Privileged Command
/// </summary>
/// <returns>True or false for success</returns>
public async Task<bool> CancelAlert()
{
return await InitiateCommandAndWaitForSuccess("cancelAlert", null);
}
}
}

View File

@@ -1,172 +0,0 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
namespace GM.Api.Models
{
/// <summary>
/// Model of the encrypted Andorid configuration file
/// </summary>
public class GmConfiguration
{
/// <summary>
/// Client Credentials by GM Brand
/// </summary>
[JsonProperty("brand_client_info")]
public Dictionary<string, BrandClientInfo> BrandClientInfo { get; set; }
/// <summary>
/// Endpoint Configuration collection
/// </summary>
[JsonProperty("configs")]
public ApiConfig[] Configs { get; set; }
/// <summary>
/// Presumably configuration used for navigation
/// </summary>
[JsonProperty("telenav_config")]
public TelenavConfig TelenavConfig { get; set; }
/// <summary>
/// Unknown
/// </summary>
[JsonProperty("equip_key")]
public string EquipKey { get; set; }
/// <summary>
/// Probably the key used to encrypt the saved OnStar PINs
/// </summary>
[JsonProperty("key_store_password")]
public string KeyStorePassword { get; set; }
/// <summary>
/// Unknown
/// </summary>
[JsonProperty("key_password")]
public string KeyPassword { get; set; }
/// <summary>
/// Certificate pinning information used to prevent SSL spoofing
/// </summary>
[JsonProperty("certs")]
public Dictionary<string, RegionCert> Certs { get; set; }
}
/// <summary>
/// Client Credentials for a given GM brand
/// </summary>
public class BrandClientInfo
{
/// <summary>
/// OAuth Client ID
/// </summary>
[JsonProperty("client_id")]
public string ClientId { get; set; }
/// <summary>
/// OAuth Client Secret
/// </summary>
[JsonProperty("client_secret")]
public string ClientSecret { get; set; }
/// <summary>
/// Debug environment Oauth Client ID
/// </summary>
[JsonProperty("debug_client_id")]
public string DebugClientId { get; set; }
/// <summary>
/// Debug environment Oauth Client Secret
/// </summary>
[JsonProperty("debug_client_secret")]
public string DebugClientSecret { get; set; }
}
/// <summary>
/// API configuration for a given GM brand
/// </summary>
public class ApiConfig
{
/// <summary>
/// GM Brand name
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// Base API endpoint URL (eg "https://api.gm.com/api")
/// </summary>
[JsonProperty("url")]
public string Url { get; set; }
/// <summary>
/// Space separated scopes required for login
/// </summary>
[JsonProperty("required_client_scope")]
public string RequiredClientScope { get; set; }
/// <summary>
/// Space separated scopes optional for login
/// </summary>
[JsonProperty("optional_client_scope")]
public string OptionalClientScope { get; set; }
/// <summary>
/// Use the Brand config Client ID instead
/// </summary>
[JsonProperty("client_id")]
public string ClientId { get; set; }
/// <summary>
/// Use the Brand config Client Secret instead
/// </summary>
[JsonProperty("client_secret")]
public string ClientSecret { get; set; }
}
/// <summary>
/// Client credentials for Telenav system
/// </summary>
public class TelenavConfig
{
/// <summary>
/// Name
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// OAuth Client ID
/// </summary>
[JsonProperty("client_id")]
public string ClientId { get; set; }
/// <summary>
/// OAuth Client Secret
/// </summary>
[JsonProperty("client_secret")]
public string ClientSecret { get; set; }
}
/// <summary>
/// Container for certificate pinning info
/// </summary>
public class RegionCert
{
/// <summary>
/// Pattern used by the expected certificate. Should match the CN I'm guessing
/// </summary>
[JsonProperty("pattern")]
public string Pattern { get; set; }
/// <summary>
/// A list of certificate pins. There are usually 3 and only one matches the actual SSL cert of the server
/// The other two do not match the intermediate / root certs - not sure what they are
/// </summary>
[JsonProperty("certificate_pins")]
public string[] CertificatePins { get; set; }
}
}

View File

@@ -8,7 +8,7 @@ using System.Text;
namespace GM.Api.Tokens
{
class JwtTool
public class JwtTool
{
public IJwtEncoder Encoder { get; private set; }
@@ -28,6 +28,7 @@ namespace GM.Api.Tokens
IDateTimeProvider dateTimeProvider = new UtcDateTimeProvider();
IJwtValidator validator = new JwtValidator(serializer, dateTimeProvider);
Decoder = new JwtDecoder(serializer, validator, urlEncoder);
}

View File

@@ -4,6 +4,9 @@
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<section name="GM.WindowsUI.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
</sectionGroup>
<sectionGroup name="applicationSettings" type="System.Configuration.ApplicationSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="GM.WindowsUI.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</sectionGroup>
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
@@ -35,4 +38,11 @@
</dependentAssembly>
</assemblyBinding>
</runtime>
<applicationSettings>
<GM.WindowsUI.Properties.Settings>
<setting name="TokenSignerUrl" serializeAs="String">
<value>https://gmsigner.herokuapp.com/</value>
</setting>
</GM.WindowsUI.Properties.Settings>
</applicationSettings>
</configuration>

View File

@@ -1,4 +1,5 @@
using GM.Api.Models;
using GM.Api;
using GM.Api.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
@@ -22,27 +23,24 @@ namespace GM.WindowsUI
/// </summary>
public partial class BrandWindow : Window
{
public Brand? SelectedBrand { get; set; } = null;
GmConfiguration _config;
public string SelectedBrand { get; set; } = null;
public BrandWindow(GmConfiguration configuration)
public BrandWindow()
{
_config = configuration;
InitializeComponent();
foreach (var brandName in _config.BrandClientInfo.Keys.OrderBy((val) => val, StringComparer.OrdinalIgnoreCase))
var brandNames = Enum.GetNames(typeof(Brand));
foreach (var brandName in brandNames.OrderBy((val) => val, StringComparer.OrdinalIgnoreCase))
{
lstBrands.Items.Add(brandName.Substring(0, 1).ToUpperInvariant() + brandName.Substring(1));
lstBrands.Items.Add(brandName);
}
}
private void BtnOk_Click(object sender, RoutedEventArgs e)
{
if (lstBrands.SelectedItem == null) return;
SelectedBrand = ((string)lstBrands.SelectedItem).ToLowerInvariant();
SelectedBrand = BrandHelpers.GetBrand((string)lstBrands.SelectedItem);
this.Close();
}
}

View File

@@ -25,17 +25,9 @@ namespace GM.WindowsUI
/// </summary>
public partial class MainWindow : Window
{
GenericGMClient _client;
GmConfiguration _globalConfig;
ApiConfig _apiConfig;
BrandClientInfo _clientCredentials;
string _brand;
string _brandDisplay;
GMClientBase _client;
Brand _brand;
Vehicle[] _vehicles = null;
@@ -44,7 +36,6 @@ namespace GM.WindowsUI
public MainWindow()
{
InitializeComponent();
LoadConfiguration();
LoadBrand();
CreateClient();
grpActions.IsEnabled = false;
@@ -60,7 +51,7 @@ namespace GM.WindowsUI
}
//todo: maybe the client reads the config and takes the brand and device id as param?
_client = new GenericGMClient(_clientCredentials.ClientId, Properties.Settings.Default.DeviceId, _clientCredentials.ClientSecret, _apiConfig.Url);
_client = new GMClientNoKey(Properties.Settings.Default.DeviceId, _brand, Properties.Settings.Default.TokenSignerUrl);
_client.TokenUpdateCallback = TokenUpdateHandler;
if (!string.IsNullOrEmpty(Properties.Settings.Default.LoginData))
@@ -100,52 +91,23 @@ namespace GM.WindowsUI
{
if (string.IsNullOrEmpty(Properties.Settings.Default.Brand))
{
var bw = new BrandWindow(_globalConfig);
var bw = new BrandWindow();
bw.ShowDialog();
if (string.IsNullOrEmpty(bw.SelectedBrand))
if (!bw.SelectedBrand.HasValue)
{
MessageBox.Show("You must select a brand!");
Environment.Exit(100);
return;
}
Properties.Settings.Default.Brand = bw.SelectedBrand;
Properties.Settings.Default.Brand = bw.SelectedBrand.Value.GetName();
Properties.Settings.Default.Save();
}
_brand = Properties.Settings.Default.Brand;
_brandDisplay = _brand.Substring(0, 1).ToUpperInvariant() + _brand.Substring(1);
_brand = BrandHelpers.GetBrand(Properties.Settings.Default.Brand);
Title = _brandDisplay + " Vehicle Control";
_clientCredentials = _globalConfig.BrandClientInfo[_brand];
_apiConfig = (from f in _globalConfig.Configs where f.Name.Equals(_brand, StringComparison.OrdinalIgnoreCase) select f).FirstOrDefault();
}
void LoadConfiguration()
{
if (!Directory.Exists("apk")) Directory.CreateDirectory("apk");
var fn = (from f in Directory.EnumerateFiles("apk") where System.IO.Path.GetExtension(f).Equals(".apk", StringComparison.OrdinalIgnoreCase) select f).FirstOrDefault();
if (string.IsNullOrEmpty(fn))
{
MessageBox.Show("You must copy the Android app's .apk file to the apk folder first.", "Missing apk");
Environment.Exit(100);
return;
}
try
{
_globalConfig = JsonConvert.DeserializeObject<GmConfiguration>(GM.SettingsReader.ReadUtility.Read(Properties.Resources.a, Properties.Resources.gm, File.OpenRead(fn)));
}
catch (Exception ex)
{
MessageBox.Show("Error reading config file: " + ex.ToString(), "Config read error");
Environment.Exit(100);
return;
}
Title = _brand.GetDisplayName() + " Vehicle Control";
}

View File

@@ -82,5 +82,14 @@ namespace GM.WindowsUI.Properties {
this["Vin"] = value;
}
}
[global::System.Configuration.ApplicationScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("https://gmsigner.herokuapp.com/")]
public string TokenSignerUrl {
get {
return ((string)(this["TokenSignerUrl"]));
}
}
}
}

View File

@@ -17,5 +17,8 @@
<Setting Name="Vin" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
<Setting Name="TokenSignerUrl" Type="System.String" Scope="Application">
<Value Profile="(Default)">https://gmsigner.herokuapp.com/</Value>
</Setting>
</Settings>
</SettingsFile>

3
GM.sln
View File

@@ -9,8 +9,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GM.WindowsUI", "GM.WindowsU
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8DADF6E3-2511-4EE4-849C-CC71C89CBD7E}"
ProjectSection(SolutionItems) = preProject
Assemblies\GM.SettingsReader.dll = Assemblies\GM.SettingsReader.dll
readme.md = readme.md
README.md = README.md
EndProjectSection
EndProject
Global

View File

@@ -15,20 +15,20 @@ You are accepting all responsibility and liability for the use of this content.
# Client Credentials
To use this API you will require a valid client id and client secret. The correct approach would be to request access from GM at https://developer.gm.com/ or by emailing them at developer.gm.com.
Alternatively (and because GM refuses to respond to developer requests) you can extract the credentials from the Android app's .apk file.
I am _NOT_ including the source code for this process, but I have included the capability. GM.SettingsReader.dll can do this. I have obfuscated the process.
I have a utility that is capable of extracting the client credentials from the Android APK but I am _very_ hesitant to share. The client secrets get out and GM will freak out. So...
IMPORTANT: The demo app requires a copy of the Android app's .apk file to be copied to the "apk" folder. It has been tested with the myChevrolet app, version 3.21.0.
VERY IMPORTANT: Unless you want an international incident on your hands DO NOT SHARE ANY OF THE CONTENTS OF THE SETTINGS FILE ANYWHERE _EVER_!!!!
I have implemented a very small, very simple web service hosted with heroku (https://gmsigner.herokuapp.com/) that will sign token requests for you,
and I have implemented a version of the client that uses this service.
(Please note that you will be sending your login credentials to this service. I would highly advise reviewing the service source code here: https://github.com/q39JzrRa/GM-Vehicle-API-AuthUtil . It is deployed to Heroku using CD from the master branch so the code you see is the code that runs)
# Quick Start
If you prefer not to use the Windows UI or examine how it works, here is how you might start your car if you only have one.
```
// Obtain Client ID, generate Device ID (GUID formatted as a string), Obtain Client Secret
// generate Device ID (GUID formatted as a string)
var client = new GenericGMClient("{client id}", "{deviceId}", "{client secret}", "https:\\api.gm.com\api");
var client = new GMClientNoKey("{deviceId}", Brand.Chevrolet, "https://gmsigner.herokuapp.com/");
if (!await _client.Login("{ your username}", "{your password}")) { throw new InvalidOperationException("Login Failed"); }
var vehicles = await _client.GetVehicles();
if (vehicles == null || !vehicles.Any()) { throw new InvalidOperationException("No Vehicles on acccount"); }