From 4bb20b3e883912fc5468fe2492d9070cec9e367f Mon Sep 17 00:00:00 2001 From: Treeki Date: Thu, 30 Jun 2016 04:00:55 +0200 Subject: initial commit --- ArchSession.cs | 527 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 527 insertions(+) create mode 100755 ArchSession.cs (limited to 'ArchSession.cs') diff --git a/ArchSession.cs b/ArchSession.cs new file mode 100755 index 0000000..d080558 --- /dev/null +++ b/ArchSession.cs @@ -0,0 +1,527 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Net; +using Newtonsoft.Json.Linq; +using System.IO; +using System.Security.Cryptography; + +namespace ArchBreaker +{ + static class Constants + { + public const string ClientID = "2c44652b8960c92e"; + public const string RedirectURI = "npf" + ClientID + "://auth"; + public const string OAuthURL = "https://accounts.nintendo.com/connect/1.0.0/authorize"; + + public const string BaasBaseURL = "https://a01202e031d911e58fa2efb70468ccd2.baas.nintendo.com"; + public const string MiitomoBaseURL = "https://api.miitomo.com"; + public const string AccountsBaseURL = "https://accounts.nintendo.com"; + public const string ApiAccountsBaseURL = "https://api.accounts.nintendo.com"; + + public const string LoginURL = BaasBaseURL + "/1.0.0/gateway/sdk/login"; + public const string TokenURL = ApiAccountsBaseURL + "/1.0.0/gateway/sdk/token"; + public const string SessionTokenURL = AccountsBaseURL + "/connect/1.0.0/api/session_token"; + public const string FederationURL = BaasBaseURL + "/1.0.0/gateway/sdk/federation"; + + public const string MiiSessionURL = MiitomoBaseURL + "/v1/session"; + public const string MiiPlayerURL = MiitomoBaseURL + "/v1/player"; + } + + class ArchSession + { + public class ConfigParam + { + public string Key { get; } + public string Value { get; set; } + + public ConfigParam(string key, string value) + { + Key = key; + Value = value; + } + } + + private List _config = new List(); + public IEnumerable Config { get { return _config; } } + private WebProxy _proxy; + + private readonly string[] iOSDevices = { + "iPhone5,1", "iPhone5,2", "iPhone5,3", "iPhone5,4", + "iPhone6,1", "iPhone6,2", "iPhone7,1", "iPhone7,2", + "iPhone8,1", "iPhone8,2", "iPhone8,3", + "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4", + "iPad2,5", "iPad2,6", "iPad2,7", + "iPad3,1", "iPad3,2", "iPad3,3", "iPad3,4", "iPad3,5", "iPad3,6", + "iPad4,1", "iPad4,2", "iPad4,3", "iPad4,4", "iPad4,5", + "iPad4,6", "iPad4,7", "iPad4,8", "iPad4,9", + "iPad5,1", "iPad5,2", "iPad5,3", "iPad5,4", + "iPad6,3", "iPad6,4", "iPad6,7", "iPad6,8", + "iPod5,1", "iPod7,1" + }; + private readonly string[] iOSVersions = + { + "9.0", "9.0.1", "9.0.2", "9.1", "9.2", "9.2.1", + "9.3", "9.3.1", "9.3.2", "9.3.3" + }; + + public ArchSession() + { + var random = new Random(); + + _config.Add(new ConfigParam("NPFDeviceAccount", "")); + _config.Add(new ConfigParam("NPFDevicePassword", "")); + _config.Add(new ConfigParam("MiitomoSessionID", "")); + _config.Add(new ConfigParam("AdvertisingID", Guid.NewGuid().ToString().ToUpper())); + _config.Add(new ConfigParam("CommonKey", "")); + _config.Add(new ConfigParam("TokenIssuer", "")); + _config.Add(new ConfigParam("Manufacturer", "Apple")); + _config.Add(new ConfigParam("DeviceName", iOSDevices[random.Next(iOSDevices.Length)])); + _config.Add(new ConfigParam("OSType", "iOS")); + _config.Add(new ConfigParam("OSVersion", iOSVersions[random.Next(iOSVersions.Length)])); + _config.Add(new ConfigParam("TimeZoneOffset", "7200000")); + _config.Add(new ConfigParam("TimeZone", "Europe/Madrid")); + _config.Add(new ConfigParam("Locale", "en-GB")); + _config.Add(new ConfigParam("Country", "GB")); + _config.Add(new ConfigParam("NetworkType", "wifi")); + _config.Add(new ConfigParam("MiitomoVersion", "1.2.3")); + _config.Add(new ConfigParam("NPFSDKVersion", "1.0.6-1d50eaf")); + _config.Add(new ConfigParam("SakashoSDKVersion", "3.2.0")); + + _proxy = new WebProxy("http://192.168.1.70:8888"); + } + + public ConfigParam GetParam(string key) + { + return _config.First(p => p.Key == key); + } + + + public class LogEventArgs : EventArgs + { + public string Message; + } + public event EventHandler OnLog; + private void Log(string message) + { + OnLog?.Invoke(this, new LogEventArgs { Message = message }); + } + + + public async Task CallMiitomoAPIAsync(string endpoint, string method, JObject payload, string sessionId = null) + { + var key = ArchSecurity.ComputeKey( + GetParam("CommonKey").Value, + sessionId ?? GetParam("MiitomoSessionID").Value); + + string url = Constants.MiitomoBaseURL; + url += (endpoint.StartsWith("/") ? endpoint : ("/" + endpoint)); + + var request = CreateRequest(url, method) as HttpWebRequest; + + request.CookieContainer = new CookieContainer(1); + request.CookieContainer.Add(new Cookie( + "player_session_id", + sessionId ?? GetParam("MiitomoSessionID").Value, + "/", "api.miitomo.com")); + + if ((method == "POST" || method == "PUT") && payload != null) + { + var jsonBytes = Encoding.UTF8.GetBytes(payload.ToString()); + var compBytes = ArchSecurity.CompressPacket(jsonBytes); + ArchSecurity.TransformPacket(compBytes, key, true); + + request.ContentLength = compBytes.Length; + request.ContentType = "application/json"; + + var reqstream = await request.GetRequestStreamAsync(); + reqstream.Write(compBytes, 0, compBytes.Length); + reqstream.Close(); + } + + HttpWebResponse response; + try + { + response = await request.GetResponseAsync() as HttpWebResponse; + } + catch (WebException ex) + { + Log(ex.ToString()); + return null; + } + + // We've got something + var stream = response.GetResponseStream(); + var reader = new BinaryReader(stream); + var blob = reader.ReadBytes((int) response.ContentLength); + + ArchSecurity.TransformPacket(blob, key, false); + blob = ArchSecurity.DecompressPacket(blob); + + return JObject.Parse(Encoding.UTF8.GetString(blob)); + } + + + public class NewAccountResult + { + public string OriginalUserID, OriginalIDToken; + public string DeviceAccount, DevicePassword; + public string NPFSessionID, MiitomoSessionID; + public string OAuthURL, Verifier; + } + public async Task CreateNewAccountAsync() + { + // Step 1: Create an account + Log("-----\r\n[1] Creating a new device account."); + + var loginReq = await CreateLoginRequestAsync(null, null); + var loginResp = await loginReq.GetResponseAsync(); + var login = await HandleJSONResponseAsync(loginResp); + + var devAccInfo = login["createdDeviceAccount"]; + var devAccount = devAccInfo["id"].ToObject(); + var devPassword = devAccInfo["password"].ToObject(); + var accessToken = login["accessToken"].ToObject(); + var idToken = login["idToken"].ToObject(); + var userId = login["user"]["id"].ToObject(); + var sessionId = login["sessionId"].ToObject(); + + Log(string.Format("Account created ({0} :: {1})", devAccount, devPassword)); + + // Step 2: Create a Miitomo player + Log("-----\r\n[2] Creating a Miitomo player"); + + var playerReq = await CreateMiiPlayerRequestAsync(idToken); + var playerResp = await playerReq.GetResponseAsync(); + var player = await HandleJSONResponseAsync(playerResp); + var playerId = player["player_id"].ToObject(); + + // Step 3: Create a Miitomo session + Log("-----\r\n[3] Logging into Miitomo"); + + var miiSessionId = await AuthToMiitomoAsync(idToken); + Log("Session ID obtained: " + miiSessionId); + + // Step 4: OAuth! + var state = ArchSecurity.RandomString(50); + var verifier = ArchSecurity.RandomString(50); + var verifierHash = SHA256.Create().ComputeHash(Encoding.ASCII.GetBytes(verifier)); + + var oauthUrl = string.Format( + "{0}?state={1}&redirect_uri={2}&client_id={3}&lang={4}" + + "&scope={5}&response_type={6}&profile_source={7}" + + "&session_token_code_challenge={8}" + + "&session_token_code_challenge_method={9}", + + Constants.OAuthURL, state, + Uri.EscapeDataString(Constants.RedirectURI), Constants.ClientID, + Uri.EscapeDataString(GetParam("Locale").Value.Replace('-', '_')), + Uri.EscapeDataString("userinfo.birthday userinfo.mii openid offline userinfo.profile mission missionStatus missionCompletion members:authenticate userGift:receive"), + "session_token_code", + Uri.EscapeDataString("{\"country\":\"" + GetParam("Country").Value + "\"}"), + Uri.EscapeDataString(Jose.Base64Url.Encode(verifierHash)), + "S256"); + + Log("-----\r\n[4] Link your Nintendo Account"); + + return new NewAccountResult + { + OriginalIDToken = idToken, + OriginalUserID = userId, + NPFSessionID = sessionId, + DeviceAccount = devAccount, + DevicePassword = devPassword, + MiitomoSessionID = miiSessionId, + OAuthURL = oauthUrl, + Verifier = verifier + }; + } + + public class SessionTokenResult + { + public string SessionToken, Code; + } + public async Task HandleAuthURLAsync(string authUrl, NewAccountResult acctInfo) + { + // Extract bits from the url + int codePos = authUrl.IndexOf("&session_token_code="); + if (codePos == -1) + { + Log("No session_token_code? Welp, that's weird"); + return null; + } + + codePos += 20; + int codeEndPos = authUrl.IndexOf('&', codePos); + if (codeEndPos == -1) + codeEndPos = authUrl.Length; // just in case it's at the end! + + var sessionTokenCode = authUrl.Substring(codePos, codeEndPos - codePos); + var request = await CreateSessionTokenRequestAsync(sessionTokenCode, acctInfo.Verifier); + var response = await request.GetResponseAsync(); + var result = await HandleJSONResponseAsync(response); + + Log("-----\r\nObtained session_token and code"); + + return new SessionTokenResult + { + SessionToken = result["session_token"].ToObject(), + Code = result["code"].ToObject() + }; + } + + public class AccountTokenResult + { + public string UserID, IDToken; + } + public async Task GetAccountTokenAsync(string sessionToken) + { + Log(string.Format("-----\r\nLinking session token {0} ...", sessionToken)); + + var request = await CreateAccountTokenRequestAsync(sessionToken); + var response = await request.GetResponseAsync(); + var result = await HandleJSONResponseAsync(response); + + Log(result.ToString()); + return new AccountTokenResult + { + UserID = result["user"]["id"].ToObject(), + IDToken = result["idToken"].ToObject() + }; + } + + + public async Task DoFederationAsync(string idToken, string sessionId, string previousUserId, string deviceAccount, string devicePassword) + { + var payload = CreateBaseLoginParameters(deviceAccount, devicePassword); + payload["sessionId"] = sessionId; + payload["previousUserId"] = previousUserId; + payload["idpAccount"] = new JObject + { + { "idToken", idToken }, + { "idp", "nintendoAccount" } + }; + + var request = await CreateJSONRequestAsync(Constants.FederationURL, "POST", payload); + var response = await request.GetResponseAsync(); + var result = await HandleJSONResponseAsync(response); + + return result["idToken"].ToObject(); + } + + + public async Task NPFLoginAsync(string account, string password) + { + Log(string.Format("Logging in (device creds {0} :: {1})", account, password)); + + var loginReq = await CreateLoginRequestAsync(account, password); + WebResponse loginResp; + try + { + loginResp = await loginReq.GetResponseAsync(); + } + catch (WebException e) + { + Log("Login failed:\r\n" + e.ToString()); + return null; + } + + var loginBlob = await HandleJSONResponseAsync(loginResp); + return loginBlob["idToken"].ToObject(); + } + + public async Task AuthToMiitomoAsync(string idToken) + { + var sessionReq = await CreateMiiSessionRequestAsync(idToken); + WebResponse sessionResp; + try + { + sessionResp = await sessionReq.GetResponseAsync(); + } + catch (WebException e) + { + Log("Session creation failed:\r\n" + e.ToString()); + return null; + } + + // .NET won't let me read the cookie directly because it's HTTP- + // only, so I have to parse the Set-Cookie header + var cookieHeader = sessionResp.Headers[HttpResponseHeader.SetCookie]; + sessionResp.Close(); + + int psidPos = cookieHeader.IndexOf("player_session_id="); + if (psidPos >= 0) + { + int psidEndPos = cookieHeader.IndexOf(';', psidPos); + + psidPos += 18; + return cookieHeader.Substring(psidPos, psidEndPos - psidPos); + } + else + { + Log("Login failed; could not find player_session_id in cookie!\r\n" + cookieHeader); + return null; + } + } + + + private JObject CreateBaseLoginParameters(string deviceAccount = null, string devicePassword = null) + { + var root = new JObject + { + {"timeZoneOffset", int.Parse(GetParam("TimeZoneOffset").Value)}, + {"advertisingId", GetParam("AdvertisingID").Value}, + {"osVersion", GetParam("OSVersion").Value}, + {"networkType", GetParam("NetworkType").Value}, + {"locale", GetParam("Locale").Value}, + {"osType", GetParam("OSType").Value}, + {"deviceName", GetParam("DeviceName").Value}, + {"timeZone", GetParam("TimeZone").Value}, + {"manufacturer", GetParam("Manufacturer").Value}, + {"assertion", ArchSecurity.GenerateAssertion(GetParam("TokenIssuer").Value)}, + {"appVersion", GetParam("MiitomoVersion").Value}, + {"sdkVersion", GetParam("NPFSDKVersion").Value}, + //{"carrier", GetParam("Carrier").Value} -- this is optional! + }; + + if (deviceAccount != null && devicePassword != null) + { + root["deviceAccount"] = new JObject + { + {"id", deviceAccount}, + {"password", devicePassword} + }; + } + + return root; + } + + + private async Task CreateLoginRequestAsync(string deviceAccount, string devicePassword) + { + var root = CreateBaseLoginParameters(deviceAccount, devicePassword); + return await CreateJSONRequestAsync(Constants.LoginURL, "POST", root); + } + + private async Task CreateMiiPlayerRequestAsync(string idToken) + { + var root = new JObject { { "id_token", idToken } }; + return await CreateJSONRequestAsync(Constants.MiiPlayerURL, "POST", root); + } + + private async Task CreateMiiSessionRequestAsync(string idToken) + { + var root = new JObject { { "id_token", idToken } }; + return await CreateJSONRequestAsync(Constants.MiiSessionURL, "POST", root); + } + + private async Task CreateSessionTokenRequestAsync(string tokenCode, string verifier) + { + var request = CreateRequest(Constants.SessionTokenURL, "POST"); + + var payload = string.Format( + "client_id={0}&session_token_code={1}&session_token_code_verifier={2}", + Uri.EscapeDataString(Constants.ClientID), + Uri.EscapeDataString(tokenCode), + Uri.EscapeDataString(verifier)); + var payloadBytes = Encoding.ASCII.GetBytes(payload); + + request.ContentType = "application/x-www-form-urlencoded"; + request.ContentLength = payloadBytes.Length; + + var stream = await request.GetRequestStreamAsync(); + stream.Write(payloadBytes, 0, payloadBytes.Length); + stream.Close(); + + return request; + } + + private async Task CreateAccountTokenRequestAsync(string sessionToken) + { + var root = new JObject { + { "client_id", Constants.ClientID }, + { "session_token", sessionToken } + }; + return await CreateJSONRequestAsync(Constants.TokenURL, "POST", root); + } + + public WebRequest CreateRequest(string URI, string method) + { + var req = WebRequest.CreateHttp(URI); + req.Method = method; + //req.Proxy = _proxy; + req.Accept = "*/*"; + if (URI.StartsWith(Constants.MiitomoBaseURL)) + { + req.UserAgent = SakashoUserAgent; + req.Headers["X-Arch-Device-Language"] = GetParam("Locale").Value; + req.Headers["X-Arch-Device-Timezone"] = GetParam("TimeZone").Value; + req.Headers["X-Sakasho-Gameid"] = "1"; + } + else + { + req.UserAgent = NintendoUserAgent; + } + return req; + } + + private async Task CreateJSONRequestAsync(string URI, string method, JObject root) + { + var req = CreateRequest(URI, method); + + var json = root.ToString(Newtonsoft.Json.Formatting.None); + var blob = Encoding.UTF8.GetBytes(json); + + req.ContentType = "application/json"; + req.ContentLength = blob.Length; + + var stream = await req.GetRequestStreamAsync(); + stream.Write(blob, 0, blob.Length); + stream.Close(); + + return req; + } + + private async Task HandleJSONResponseAsync(WebResponse resp) + { + var stream = resp.GetResponseStream(); + var reader = new StreamReader(stream); + var blob = await reader.ReadToEndAsync(); + reader.Close(); + resp.Close(); + + return JObject.Parse(blob); + } + + + private string NintendoUserAgent + { + get + { + var appVer = GetParam("MiitomoVersion").Value; + var device = GetParam("DeviceName").Value; + var osVer = GetParam("OSVersion").Value; + var sdkVer = GetParam("NPFSDKVersion").Value; + return string.Format( + "com.nintendo.zaaa/{0} {1}/{2} NPFSDK/{3}", + appVer, device, osVer, sdkVer); + } + } + private string SakashoUserAgent + { + get + { + var appVer = GetParam("MiitomoVersion").Value; + var device = GetParam("DeviceName").Value; + var osType = GetParam("OSType").Value; + var osVer = GetParam("OSVersion").Value; + var sdkVer = GetParam("SakashoSDKVersion").Value; + return string.Format( + "SakashoClient/{0}-Native/SDK:{1}/Client:{2}/OS:{3}/Model:{4}", + osType, sdkVer, appVer, osVer, device); + } + } + } +} -- cgit v1.2.3