From 83fab5ffcf45b0c6316adb8489308ee78d8f942d Mon Sep 17 00:00:00 2001 From: Anonymous Date: Sun, 18 Aug 2019 21:05:22 -0400 Subject: [PATCH] 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 --- GM.Api/Brands.cs | 100 ++++++++ GM.Api/GMClient.cs | 30 +++ GM.Api/GMClientBase.cs | 229 +++++++++++++++++-- GM.Api/GMClientNoKey.cs | 45 ++++ GM.Api/GenericGMClient.cs | 182 --------------- GM.Api/Models/Configuration.cs | 172 -------------- GM.Api/Tokens/JwtTool.cs | 3 +- GM.WindowsUI/App.config | 10 + GM.WindowsUI/BrandWindow.xaml.cs | 20 +- GM.WindowsUI/MainWindow.xaml.cs | 54 +---- GM.WindowsUI/Properties/Settings.Designer.cs | 9 + GM.WindowsUI/Properties/Settings.settings | 3 + GM.sln | 3 +- README.md | 12 +- 14 files changed, 427 insertions(+), 445 deletions(-) create mode 100644 GM.Api/Brands.cs create mode 100644 GM.Api/GMClient.cs create mode 100644 GM.Api/GMClientNoKey.cs delete mode 100644 GM.Api/GenericGMClient.cs delete mode 100644 GM.Api/Models/Configuration.cs diff --git a/GM.Api/Brands.cs b/GM.Api/Brands.cs new file mode 100644 index 0000000..a2f0899 --- /dev/null +++ b/GM.Api/Brands.cs @@ -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"); + } + } + } +} diff --git a/GM.Api/GMClient.cs b/GM.Api/GMClient.cs new file mode 100644 index 0000000..69fc647 --- /dev/null +++ b/GM.Api/GMClient.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using GM.Api.Tokens; + +namespace GM.Api +{ + /// + /// GM Client implementation to be used when you have local access to the Client ID and Client Secret + /// + 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 EncodeLoginRequest(LoginRequest request) + { + request.ClientId = _clientId; + return _jwtTool.EncodeToken(request); + } + } +} diff --git a/GM.Api/GMClientBase.cs b/GM.Api/GMClientBase.cs index 7a56b2c..b925f22 100644 --- a/GM.Api/GMClientBase.cs +++ b/GM.Api/GMClientBase.cs @@ -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 /// /// Create a new GMClient /// - /// Client ID for authentication /// Device ID (should be in the format of a GUID) - /// Client Secret for authentication - /// Base url for the API. Usually https://api.gm.com/api - public GMClientBase(string clientId, string deviceId, string clientSecret, string apiUrl) + /// One of the supported brands from + 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 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(token); + } + + #region Client Helpers /// @@ -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(rawResponseToken); + //var loginTokenData = _jwtTool.DecodeTokenToObject(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(rawResponseToken); + var refreshData = DecodeLoginData(rawResponseToken); LoginData.AccessToken = refreshData.AccessToken; LoginData.IssuedAtUtc = refreshData.IssuedAtUtc; @@ -544,5 +555,173 @@ namespace GM.Api #endregion + + + #region Command Implementations + + /// + /// Retrieve Diagnostic data for the active vehicle + /// + /// + public async Task 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; + } + } + + + /// + /// Issue an arbitrary command + /// + /// Name of the command. Must exists in the vehicle's configuration + /// JSON parameters for the command + /// + public async Task IssueCommand(string commandName, JObject parameters = null) + { + return await InitiateCommandAndWait(commandName, parameters); + } + + /// + /// Lock the active vehicles's doors and wait for completion + /// Privileged Command + /// + /// True or false for success + public async Task LockDoor() + { + + var reqObj = new JObject() + { + ["lockDoorRequest"] = new JObject() + { + ["delay"] = 0 + } + }; + + return await InitiateCommandAndWaitForSuccess("lockDoor", reqObj); + } + + + /// + /// Fails when the hotspot is off... + /// Note: the app uses diagnotics that also fail when the hotpot is off + /// + /// + public async Task GetHotspotInfo() + { + var resp = await InitiateCommandAndWait("getHotspotInfo", null); + return resp.Body.HotspotInfo; + } + + + /// + /// 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 + /// + /// + /// + public async Task SendTBTRoute(TbtDestination destination) + { + var reqObj = new JObject() + { + ["tbtDestination"] = new JObject(destination) + }; + + return await InitiateCommandAndWaitForSuccess("sendTBTRoute", reqObj); + } + + + /// + /// Unlock the active vehicles's doors and wait for completion + /// Privileged Command + /// + /// True or false for success + public async Task UnlockDoor() + { + var reqObj = new JObject() + { + ["unlockDoorRequest"] = new JObject() + { + ["delay"] = 0 + } + }; + + return await InitiateCommandAndWaitForSuccess("unlockDoor", reqObj); + } + + /// + /// Remote start the active vehicle and wait for completion + /// Privileged Command + /// + /// True or false for success + public async Task Start() + { + return await InitiateCommandAndWaitForSuccess("start", null); + } + + /// + /// Remote stop the active vehicle and wait for completion + /// Privileged Command + /// + /// True or false for success + public async Task CancelStart() + { + return await InitiateCommandAndWaitForSuccess("cancelStart", null); + } + + + /// + /// Set off remote alarm on the active vehicle and wait for completion + /// Privileged Command + /// + /// True or false for success + public async Task 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); + } + + /// + /// Stop remote alarm on the active vehicle and wait for completion + /// Privileged Command + /// + /// True or false for success + public async Task CancelAlert() + { + return await InitiateCommandAndWaitForSuccess("cancelAlert", null); + } + + #endregion + + + } } diff --git a/GM.Api/GMClientNoKey.cs b/GM.Api/GMClientNoKey.cs new file mode 100644 index 0000000..25d6f7f --- /dev/null +++ b/GM.Api/GMClientNoKey.cs @@ -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 +{ + /// + /// 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 + /// + public class GMClientNoKey : GMClientBase + { + string _tokenSignUrl; + HttpClient _tokenClient = new HttpClient(); + + /// + /// Create new GM Client + /// + /// deviceId = string representation of a GUID + /// API is segmented by brand + /// URL for webservice that will sign JWT tokens (e.g. "https://gmsigner.herokuapp.com/") + public GMClientNoKey(string deviceId, Brand brand, string tokenSignUrl) : base(deviceId, brand) + { + _tokenSignUrl = tokenSignUrl; + } + + protected override async Task 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); + } + } + } +} diff --git a/GM.Api/GenericGMClient.cs b/GM.Api/GenericGMClient.cs deleted file mode 100644 index 5646aa9..0000000 --- a/GM.Api/GenericGMClient.cs +++ /dev/null @@ -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 -{ - /// - /// Generic implementation of GM Client supporting a limited set of commands - /// - public class GenericGMClient : GMClientBase - { - public GenericGMClient(string clientId, string deviceId, string clientSecret, string apiUrl) : base(clientId, deviceId, clientSecret, apiUrl) - { - } - - /// - /// Retrieve Diagnostic data for the active vehicle - /// - /// - public async Task 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; - } - } - - - /// - /// Issue an arbitrary command - /// - /// Name of the command. Must exists in the vehicle's configuration - /// JSON parameters for the command - /// - public async Task IssueCommand(string commandName, JObject parameters = null) - { - return await InitiateCommandAndWait(commandName, parameters); - } - - /// - /// Lock the active vehicles's doors and wait for completion - /// Privileged Command - /// - /// True or false for success - public async Task LockDoor() - { - - var reqObj = new JObject() - { - ["lockDoorRequest"] = new JObject() - { - ["delay"] = 0 - } - }; - - return await InitiateCommandAndWaitForSuccess("lockDoor", reqObj); - } - - - /// - /// Fails when the hotspot is off... - /// Note: the app uses diagnotics that also fail when the hotpot is off - /// - /// - public async Task GetHotspotInfo() - { - var resp = await InitiateCommandAndWait("getHotspotInfo", null); - return resp.Body.HotspotInfo; - } - - - /// - /// 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 - /// - /// - /// - public async Task SendTBTRoute(TbtDestination destination) - { - var reqObj = new JObject() - { - ["tbtDestination"] = new JObject(destination) - }; - - return await InitiateCommandAndWaitForSuccess("sendTBTRoute", reqObj); - } - - - /// - /// Unlock the active vehicles's doors and wait for completion - /// Privileged Command - /// - /// True or false for success - public async Task UnlockDoor() - { - var reqObj = new JObject() - { - ["unlockDoorRequest"] = new JObject() - { - ["delay"] = 0 - } - }; - - return await InitiateCommandAndWaitForSuccess("unlockDoor", reqObj); - } - - /// - /// Remote start the active vehicle and wait for completion - /// Privileged Command - /// - /// True or false for success - public async Task Start() - { - return await InitiateCommandAndWaitForSuccess("start", null); - } - - /// - /// Remote stop the active vehicle and wait for completion - /// Privileged Command - /// - /// True or false for success - public async Task CancelStart() - { - return await InitiateCommandAndWaitForSuccess("cancelStart", null); - } - - - /// - /// Set off remote alarm on the active vehicle and wait for completion - /// Privileged Command - /// - /// True or false for success - public async Task 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); - } - - /// - /// Stop remote alarm on the active vehicle and wait for completion - /// Privileged Command - /// - /// True or false for success - public async Task CancelAlert() - { - return await InitiateCommandAndWaitForSuccess("cancelAlert", null); - } - - - - - } -} diff --git a/GM.Api/Models/Configuration.cs b/GM.Api/Models/Configuration.cs deleted file mode 100644 index fe75ffb..0000000 --- a/GM.Api/Models/Configuration.cs +++ /dev/null @@ -1,172 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Text; - -namespace GM.Api.Models -{ - /// - /// Model of the encrypted Andorid configuration file - /// - - public class GmConfiguration - { - /// - /// Client Credentials by GM Brand - /// - [JsonProperty("brand_client_info")] - public Dictionary BrandClientInfo { get; set; } - - /// - /// Endpoint Configuration collection - /// - [JsonProperty("configs")] - public ApiConfig[] Configs { get; set; } - - /// - /// Presumably configuration used for navigation - /// - [JsonProperty("telenav_config")] - public TelenavConfig TelenavConfig { get; set; } - - /// - /// Unknown - /// - [JsonProperty("equip_key")] - public string EquipKey { get; set; } - - /// - /// Probably the key used to encrypt the saved OnStar PINs - /// - [JsonProperty("key_store_password")] - public string KeyStorePassword { get; set; } - - /// - /// Unknown - /// - [JsonProperty("key_password")] - public string KeyPassword { get; set; } - - /// - /// Certificate pinning information used to prevent SSL spoofing - /// - [JsonProperty("certs")] - public Dictionary Certs { get; set; } - } - - /// - /// Client Credentials for a given GM brand - /// - public class BrandClientInfo - { - /// - /// OAuth Client ID - /// - [JsonProperty("client_id")] - public string ClientId { get; set; } - - /// - /// OAuth Client Secret - /// - [JsonProperty("client_secret")] - public string ClientSecret { get; set; } - - /// - /// Debug environment Oauth Client ID - /// - [JsonProperty("debug_client_id")] - public string DebugClientId { get; set; } - - /// - /// Debug environment Oauth Client Secret - /// - [JsonProperty("debug_client_secret")] - public string DebugClientSecret { get; set; } - } - - /// - /// API configuration for a given GM brand - /// - public class ApiConfig - { - /// - /// GM Brand name - /// - [JsonProperty("name")] - public string Name { get; set; } - - /// - /// Base API endpoint URL (eg "https://api.gm.com/api") - /// - [JsonProperty("url")] - public string Url { get; set; } - - /// - /// Space separated scopes required for login - /// - [JsonProperty("required_client_scope")] - public string RequiredClientScope { get; set; } - - /// - /// Space separated scopes optional for login - /// - [JsonProperty("optional_client_scope")] - public string OptionalClientScope { get; set; } - - /// - /// Use the Brand config Client ID instead - /// - [JsonProperty("client_id")] - public string ClientId { get; set; } - - /// - /// Use the Brand config Client Secret instead - /// - [JsonProperty("client_secret")] - public string ClientSecret { get; set; } - } - - - /// - /// Client credentials for Telenav system - /// - public class TelenavConfig - { - /// - /// Name - /// - [JsonProperty("name")] - public string Name { get; set; } - - /// - /// OAuth Client ID - /// - [JsonProperty("client_id")] - public string ClientId { get; set; } - - /// - /// OAuth Client Secret - /// - [JsonProperty("client_secret")] - public string ClientSecret { get; set; } - } - - /// - /// Container for certificate pinning info - /// - public class RegionCert - { - /// - /// Pattern used by the expected certificate. Should match the CN I'm guessing - /// - [JsonProperty("pattern")] - public string Pattern { get; set; } - - /// - /// 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 - /// - [JsonProperty("certificate_pins")] - public string[] CertificatePins { get; set; } - } -} diff --git a/GM.Api/Tokens/JwtTool.cs b/GM.Api/Tokens/JwtTool.cs index cfdf856..8335396 100644 --- a/GM.Api/Tokens/JwtTool.cs +++ b/GM.Api/Tokens/JwtTool.cs @@ -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); + } diff --git a/GM.WindowsUI/App.config b/GM.WindowsUI/App.config index a8acc4e..ac2b333 100644 --- a/GM.WindowsUI/App.config +++ b/GM.WindowsUI/App.config @@ -4,6 +4,9 @@
+ +
+ @@ -35,4 +38,11 @@ + + + + https://gmsigner.herokuapp.com/ + + + \ No newline at end of file diff --git a/GM.WindowsUI/BrandWindow.xaml.cs b/GM.WindowsUI/BrandWindow.xaml.cs index 98df45a..8dd2972 100644 --- a/GM.WindowsUI/BrandWindow.xaml.cs +++ b/GM.WindowsUI/BrandWindow.xaml.cs @@ -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 ///
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(); } } diff --git a/GM.WindowsUI/MainWindow.xaml.cs b/GM.WindowsUI/MainWindow.xaml.cs index 20c1b7d..d01f549 100644 --- a/GM.WindowsUI/MainWindow.xaml.cs +++ b/GM.WindowsUI/MainWindow.xaml.cs @@ -25,17 +25,9 @@ namespace GM.WindowsUI /// public partial class MainWindow : Window { - GenericGMClient _client; + GMClientBase _client; - - GmConfiguration _globalConfig; - - ApiConfig _apiConfig; - BrandClientInfo _clientCredentials; - - string _brand; - string _brandDisplay; - + 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(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"; } diff --git a/GM.WindowsUI/Properties/Settings.Designer.cs b/GM.WindowsUI/Properties/Settings.Designer.cs index ab6f15b..2edb2ee 100644 --- a/GM.WindowsUI/Properties/Settings.Designer.cs +++ b/GM.WindowsUI/Properties/Settings.Designer.cs @@ -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"])); + } + } } } diff --git a/GM.WindowsUI/Properties/Settings.settings b/GM.WindowsUI/Properties/Settings.settings index 584bba6..6aa6103 100644 --- a/GM.WindowsUI/Properties/Settings.settings +++ b/GM.WindowsUI/Properties/Settings.settings @@ -17,5 +17,8 @@ + + https://gmsigner.herokuapp.com/ + \ No newline at end of file diff --git a/GM.sln b/GM.sln index 0bd82b1..e940f8d 100644 --- a/GM.sln +++ b/GM.sln @@ -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 diff --git a/README.md b/README.md index e7fd63d..011efd7 100644 --- a/README.md +++ b/README.md @@ -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"); }