diff options
author | Treeki <treeki@gmail.com> | 2016-06-30 04:00:55 +0200 |
---|---|---|
committer | Treeki <treeki@gmail.com> | 2016-06-30 04:00:55 +0200 |
commit | 4bb20b3e883912fc5468fe2492d9070cec9e367f (patch) | |
tree | f5d72a50c3d60d851ed8d7f6f754bab84fa8a8e0 /ArchSession.cs | |
download | ArchBreaker-4bb20b3e883912fc5468fe2492d9070cec9e367f.tar.gz ArchBreaker-4bb20b3e883912fc5468fe2492d9070cec9e367f.zip |
Diffstat (limited to '')
-rwxr-xr-x | ArchSession.cs | 527 |
1 files changed, 527 insertions, 0 deletions
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<ConfigParam> _config = new List<ConfigParam>();
+ public IEnumerable<ConfigParam> 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<LogEventArgs> OnLog;
+ private void Log(string message)
+ {
+ OnLog?.Invoke(this, new LogEventArgs { Message = message });
+ }
+
+
+ public async Task<JObject> 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<NewAccountResult> 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<string>();
+ var devPassword = devAccInfo["password"].ToObject<string>();
+ var accessToken = login["accessToken"].ToObject<string>();
+ var idToken = login["idToken"].ToObject<string>();
+ var userId = login["user"]["id"].ToObject<string>();
+ var sessionId = login["sessionId"].ToObject<string>();
+
+ 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<string>();
+
+ // 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<SessionTokenResult> 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<string>(),
+ Code = result["code"].ToObject<string>()
+ };
+ }
+
+ public class AccountTokenResult
+ {
+ public string UserID, IDToken;
+ }
+ public async Task<AccountTokenResult> 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<string>(),
+ IDToken = result["idToken"].ToObject<string>()
+ };
+ }
+
+
+ public async Task<string> 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<string>();
+ }
+
+
+ public async Task<string> 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<string>();
+ }
+
+ public async Task<string> 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<WebRequest> CreateLoginRequestAsync(string deviceAccount, string devicePassword)
+ {
+ var root = CreateBaseLoginParameters(deviceAccount, devicePassword);
+ return await CreateJSONRequestAsync(Constants.LoginURL, "POST", root);
+ }
+
+ private async Task<WebRequest> CreateMiiPlayerRequestAsync(string idToken)
+ {
+ var root = new JObject { { "id_token", idToken } };
+ return await CreateJSONRequestAsync(Constants.MiiPlayerURL, "POST", root);
+ }
+
+ private async Task<WebRequest> CreateMiiSessionRequestAsync(string idToken)
+ {
+ var root = new JObject { { "id_token", idToken } };
+ return await CreateJSONRequestAsync(Constants.MiiSessionURL, "POST", root);
+ }
+
+ private async Task<WebRequest> 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<WebRequest> 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<WebRequest> 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<JObject> 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);
+ }
+ }
+ }
+}
|