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; public static int RetryCount { get; set; } = 3;
//TODO: consistent exception throwing //TODO: consistent exception throwing
protected Brand _brand;
string _clientId; protected string _deviceId;
string _deviceId; protected string _apiUrl;
JwtTool _jwtTool;
string _apiUrl;
string _host;
HttpClient _client; HttpClient _client;
@@ -62,24 +59,20 @@ namespace GM.Api
/// <summary> /// <summary>
/// Create a new GMClient /// Create a new GMClient
/// </summary> /// </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="deviceId">Device ID (should be in the format of a GUID)</param>
/// <param name="clientSecret">Client Secret for authentication</param> /// <param name="brand">One of the supported brands from </param>
/// <param name="apiUrl">Base url for the API. Usually https://api.gm.com/api </param> public GMClientBase(string deviceId, Brand brand)
public GMClientBase(string clientId, string deviceId, string clientSecret, string apiUrl)
{ {
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; _deviceId = deviceId;
_jwtTool = new JwtTool(clientSecret); _apiUrl = brand.GetUrl();
_apiUrl = apiUrl;
var uri = new Uri(_apiUrl); var uri = new Uri(_apiUrl);
_host = uri.Host; _client = CreateClient(uri.Host);
_client = CreateClient(_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 #region Client Helpers
/// <summary> /// <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) 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 //possible transient errors
//todo: log this //todo: log this
await Task.Delay(500); await Task.Delay(500);
@@ -226,7 +235,7 @@ namespace GM.Api
{ {
var payload = new LoginRequest() var payload = new LoginRequest()
{ {
ClientId = _clientId, //ClientId = _clientId,
DeviceId = _deviceId, DeviceId = _deviceId,
Credential = onStarPin, Credential = onStarPin,
CredentialType = "PIN", CredentialType = "PIN",
@@ -234,7 +243,8 @@ namespace GM.Api
Timestamp = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffK") 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"))) 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() var payload = new LoginRequest()
{ {
ClientId = _clientId, //ClientId = _clientId,
DeviceId = _deviceId, DeviceId = _deviceId,
GrantType = "password", GrantType = "password",
Nonce = helpers.GenerateNonce(), Nonce = helpers.GenerateNonce(),
@@ -274,7 +284,8 @@ namespace GM.Api
Username = username 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)) 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; return false;
} }
var loginTokenData = _jwtTool.DecodeTokenToObject<LoginData>(rawResponseToken); //var loginTokenData = _jwtTool.DecodeTokenToObject<LoginData>(rawResponseToken);
var loginTokenData = DecodeLoginData(rawResponseToken);
LoginData = loginTokenData; LoginData = loginTokenData;
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", LoginData.AccessToken); _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", LoginData.AccessToken);
@@ -315,7 +327,6 @@ namespace GM.Api
var payload = new LoginRequest() var payload = new LoginRequest()
{ {
ClientId = _clientId,
DeviceId = _deviceId, DeviceId = _deviceId,
GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer", GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer",
Nonce = helpers.GenerateNonce(), Nonce = helpers.GenerateNonce(),
@@ -324,7 +335,7 @@ namespace GM.Api
Assertion = LoginData.IdToken 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)) 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 // 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.AccessToken = refreshData.AccessToken;
LoginData.IssuedAtUtc = refreshData.IssuedAtUtc; LoginData.IssuedAtUtc = refreshData.IssuedAtUtc;
@@ -544,5 +555,173 @@ namespace GM.Api
#endregion #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 namespace GM.Api.Tokens
{ {
class JwtTool public class JwtTool
{ {
public IJwtEncoder Encoder { get; private set; } public IJwtEncoder Encoder { get; private set; }
@@ -28,6 +28,7 @@ namespace GM.Api.Tokens
IDateTimeProvider dateTimeProvider = new UtcDateTimeProvider(); IDateTimeProvider dateTimeProvider = new UtcDateTimeProvider();
IJwtValidator validator = new JwtValidator(serializer, dateTimeProvider); IJwtValidator validator = new JwtValidator(serializer, dateTimeProvider);
Decoder = new JwtDecoder(serializer, validator, urlEncoder); 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"> <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" /> <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>
<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> </configSections>
<startup> <startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" /> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
@@ -35,4 +38,11 @@
</dependentAssembly> </dependentAssembly>
</assemblyBinding> </assemblyBinding>
</runtime> </runtime>
<applicationSettings>
<GM.WindowsUI.Properties.Settings>
<setting name="TokenSignerUrl" serializeAs="String">
<value>https://gmsigner.herokuapp.com/</value>
</setting>
</GM.WindowsUI.Properties.Settings>
</applicationSettings>
</configuration> </configuration>

View File

@@ -1,4 +1,5 @@
using GM.Api.Models; using GM.Api;
using GM.Api.Models;
using Newtonsoft.Json; using Newtonsoft.Json;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -22,27 +23,24 @@ namespace GM.WindowsUI
/// </summary> /// </summary>
public partial class BrandWindow : Window public partial class BrandWindow : Window
{ {
public Brand? SelectedBrand { get; set; } = null;
GmConfiguration _config; public BrandWindow()
public string SelectedBrand { get; set; } = null;
public BrandWindow(GmConfiguration configuration)
{ {
_config = configuration;
InitializeComponent(); 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) private void BtnOk_Click(object sender, RoutedEventArgs e)
{ {
if (lstBrands.SelectedItem == null) return; if (lstBrands.SelectedItem == null) return;
SelectedBrand = ((string)lstBrands.SelectedItem).ToLowerInvariant(); SelectedBrand = BrandHelpers.GetBrand((string)lstBrands.SelectedItem);
this.Close(); this.Close();
} }
} }

View File

@@ -25,17 +25,9 @@ namespace GM.WindowsUI
/// </summary> /// </summary>
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
GenericGMClient _client; GMClientBase _client;
Brand _brand;
GmConfiguration _globalConfig;
ApiConfig _apiConfig;
BrandClientInfo _clientCredentials;
string _brand;
string _brandDisplay;
Vehicle[] _vehicles = null; Vehicle[] _vehicles = null;
@@ -44,7 +36,6 @@ namespace GM.WindowsUI
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
LoadConfiguration();
LoadBrand(); LoadBrand();
CreateClient(); CreateClient();
grpActions.IsEnabled = false; 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? //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; _client.TokenUpdateCallback = TokenUpdateHandler;
if (!string.IsNullOrEmpty(Properties.Settings.Default.LoginData)) if (!string.IsNullOrEmpty(Properties.Settings.Default.LoginData))
@@ -100,52 +91,23 @@ namespace GM.WindowsUI
{ {
if (string.IsNullOrEmpty(Properties.Settings.Default.Brand)) if (string.IsNullOrEmpty(Properties.Settings.Default.Brand))
{ {
var bw = new BrandWindow(_globalConfig); var bw = new BrandWindow();
bw.ShowDialog(); bw.ShowDialog();
if (string.IsNullOrEmpty(bw.SelectedBrand)) if (!bw.SelectedBrand.HasValue)
{ {
MessageBox.Show("You must select a brand!"); MessageBox.Show("You must select a brand!");
Environment.Exit(100); Environment.Exit(100);
return; return;
} }
Properties.Settings.Default.Brand = bw.SelectedBrand; Properties.Settings.Default.Brand = bw.SelectedBrand.Value.GetName();
Properties.Settings.Default.Save(); Properties.Settings.Default.Save();
} }
_brand = Properties.Settings.Default.Brand; _brand = BrandHelpers.GetBrand(Properties.Settings.Default.Brand);
_brandDisplay = _brand.Substring(0, 1).ToUpperInvariant() + _brand.Substring(1);
Title = _brandDisplay + " Vehicle Control"; Title = _brand.GetDisplayName() + " 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;
}
} }

View File

@@ -82,5 +82,14 @@ namespace GM.WindowsUI.Properties {
this["Vin"] = value; 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"> <Setting Name="Vin" Type="System.String" Scope="User">
<Value Profile="(Default)" /> <Value Profile="(Default)" />
</Setting> </Setting>
<Setting Name="TokenSignerUrl" Type="System.String" Scope="Application">
<Value Profile="(Default)">https://gmsigner.herokuapp.com/</Value>
</Setting>
</Settings> </Settings>
</SettingsFile> </SettingsFile>

3
GM.sln
View File

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

View File

@@ -15,20 +15,20 @@ You are accepting all responsibility and liability for the use of this content.
# Client Credentials # 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. 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 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...
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.
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. I have implemented a very small, very simple web service hosted with heroku (https://gmsigner.herokuapp.com/) that will sign token requests for you,
VERY IMPORTANT: Unless you want an international incident on your hands DO NOT SHARE ANY OF THE CONTENTS OF THE SETTINGS FILE ANYWHERE _EVER_!!!! 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 # 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. 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"); } if (!await _client.Login("{ your username}", "{your password}")) { throw new InvalidOperationException("Login Failed"); }
var vehicles = await _client.GetVehicles(); var vehicles = await _client.GetVehicles();
if (vehicles == null || !vehicles.Any()) { throw new InvalidOperationException("No Vehicles on acccount"); } if (vehicles == null || !vehicles.Any()) { throw new InvalidOperationException("No Vehicles on acccount"); }