Table of contents:

File Information

This is a file that contains implementations of the MSNP6 and MSNP11 challenges, and the SSO authentication method's response.

Filename: msnp_challenges.cs.

File contents

//  Copyright 2025-2025 yellows111
//
//  Permission is hereby granted, free of charge, to any person obtaining a 
//  copy of this software and associated documentation files (the "Software"), 
//  to deal in the Software without restriction, including without limitation 
//  the rights to use, copy, modify, merge, publish, distribute, sublicense, 
//  and/or sell copies of the Software, and to permit persons to whom the 
//  Software is furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in 
//  all copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
//  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
//  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
//  DEALINGS IN THE SOFTWARE.

using System;
using System.Security.Cryptography;
using System.Text;
using yellowsoneoneone;

namespace yellowsoneoneone {
	public class MSNPChallenges {
		// used for challenge + privateKey concat hash
		private static MD5 md5 = MD5.Create();
		// swap bytes of a unsigned int stored as a long.
		private static long SwapEndianness32(long num) {
			byte[] intBytes = BitConverter.GetBytes(num);
			byte[] newBytes = new byte[8] {
				intBytes[3], intBytes[2], intBytes[1], intBytes[0],
				0, 0, 0, 0
			};
			return BitConverter.ToInt64(newBytes, 0);
		}
		// swap all bytes of a long.
		private static long SwapEndianness64(long num) {
			byte[] intBytes = BitConverter.GetBytes(num);
			byte[] newBytes = new byte[8] {
				intBytes[7], intBytes[6], intBytes[5], intBytes[4],
				intBytes[3], intBytes[2], intBytes[1], intBytes[0]
			};
			return BitConverter.ToInt64(newBytes, 0);
		}
		// solve the MSNP6 to MSNP10 QRY response.
		public static string SolveMSNP6Challenge(string privateKey, string challenge) {
			return BitConverter.ToString(
				md5.ComputeHash(
					Encoding.ASCII.GetBytes(challenge + privateKey)
				)
			).Replace("-", "").ToLower();
		}
		// solve the MSNP11 to MSNP21 QRY response.
		public static string SolveMSNP11Challenge(string publicKey, string privateKey, string challenge) {
			// part 1 -- private seed
			byte[] initialConstant = md5.ComputeHash(Encoding.ASCII.GetBytes(challenge + privateKey));
			int[] privateSeed = new int[4];
			for(int i = 0; i < 4; i++) {
				privateSeed[i] = BitConverter.ToInt32(initialConstant, i * 4) & 0x7FFFFFFF;
			}
			
			//part 2 -- public seed
			string publicString = challenge + publicKey;
			publicString = publicString.PadRight(publicString.Length + (8 - publicString.Length % 8), '0');
			byte[] publicBytes = Encoding.ASCII.GetBytes(publicString);
			int[] publicSeed = new int[publicString.Length / 4];
			for(int i = 0; i < publicSeed.Length; i++) {
				publicSeed[i] = BitConverter.ToInt32(publicBytes, i * 4) & 0x7FFFFFFF;
			}
			
			// part 3 -- key generation, the modulos instead of bitwise AND is intentional here
			long temp = 0, high = 0, low = 0;
			for (int i = 0; i < publicSeed.Length; i += 2) {
				temp = publicSeed[i];
				temp = ((temp * 0x0E79A9C1) % 0x7FFFFFFF) + high;
				temp = ((temp * privateSeed[0]) + privateSeed[1]) % 0x7FFFFFFF;
				
				high = (publicSeed[i + 1] + temp) % 0x7FFFFFFF;
				high = ((high * privateSeed[2]) + privateSeed[3]) % 0x7FFFFFFF;
				
				low += high + temp;
			}
			// by the way, requiring endian swaps isn't documentated on MSNPiki.
			// This sucked to figure out.
			high = SwapEndianness32((high + privateSeed[1]) % 0x7FFFFFFF);
			low = SwapEndianness32((low + privateSeed[3]) % 0x7FFFFFFF);
			long key = SwapEndianness64((high << 32) + low);
			
			// part 4 -- Bitwise XOR the original MD5 with our new key
			long resultHigh = BitConverter.ToInt64(initialConstant, 0) ^ key;
			long resultLow = BitConverter.ToInt64(initialConstant, 8) ^ key;
			return (
				BitConverter.ToString(BitConverter.GetBytes(resultHigh)) +
				BitConverter.ToString(BitConverter.GetBytes(resultLow))
			).Replace("-", "").ToLower();
		}
	}
	public class AuthChallenges {
		private static byte[] hashConstant = Encoding.ASCII.GetBytes(
			"WS-SecureConversationSESSION KEY HASH"
		);
		private static byte[] encryptionConstant = Encoding.ASCII.GetBytes(
			"WS-SecureConversationSESSION KEY ENCRYPTION"
		);
		// solve the MBI_KEY_OLD challenge used in MSNP15 to MSNP21.
		public static string SolveSSOChallenge(string binarySecretString, string nonceString, string ivString) {
			// sanity check
			if(binarySecretString.Length != 32) {
				throw new Exception("binarySecret is the wrong size! It should be 32 characters.");
			};
			if(nonceString.Length != 64) {
				throw new Exception("nonce is the wrong size! It should be 64 characters.");
			};
			if(ivString.Length != 12) {
				throw new Exception("iv is the wrong size! It should be 12 characters.");
			};

			// place initial values into buffers
			byte[] nonce = new byte[64];
			Buffer.BlockCopy(Encoding.ASCII.GetBytes(nonceString), 0, nonce, 0, 64);
			byte[] binarySecret = Convert.FromBase64String(binarySecretString);
			if(binarySecret.Length != 24) {
				throw new Exception("binarySecret isn't 24 bytes! Is it truncated?");
			}
			byte[] iv = Convert.FromBase64String(ivString);
			if(iv.Length != 8) {
				throw new Exception("iv isn't 8 bytes! Is it truncated?");
			}

			// key1 -- Session Key Hash
			// byte[] key1 = binarySecret;
			HMACSHA1 hmKey1 = new HMACSHA1(binarySecret);
			byte[] hash1 = hmKey1.ComputeHash(hashConstant);
			byte[] hash1constant = new byte[57];
			Buffer.BlockCopy(hash1, 0, hash1constant, 0, 20);
			Buffer.BlockCopy(hashConstant, 0, hash1constant, 20, 37);
			byte[] hash2 = hmKey1.ComputeHash(hash1constant);
			byte[] hash3 = hmKey1.ComputeHash(hash1);
			byte[] hash3constant = new byte[57];
			Buffer.BlockCopy(hash3, 0, hash3constant, 0, 20);
			Buffer.BlockCopy(hashConstant, 0, hash3constant, 20, 37);
			byte[] hash4 = hmKey1.ComputeHash(hash3constant);

			// key2 -- Session Key Encryption
			byte[] key2 = new byte[24];
			Buffer.BlockCopy(hash2, 0, key2, 0, 20);
			Buffer.BlockCopy(hash4, 0, key2, 20, 4);
			byte[] hash5 = hmKey1.ComputeHash(encryptionConstant);
			byte[] hash5constant = new byte[63];
			Buffer.BlockCopy(hash5, 0, hash5constant, 0, 20);
			Buffer.BlockCopy(encryptionConstant, 0, hash5constant, 20, 43);
			byte[] hash6 = hmKey1.ComputeHash(hash5constant);
			byte[] hash7 = hmKey1.ComputeHash(hash5);
			byte[] hash7constant = new byte[63];
			Buffer.BlockCopy(hash7, 0, hash7constant, 0, 20);
			Buffer.BlockCopy(encryptionConstant, 0, hash7constant, 20, 43);
			byte[] hash8 = hmKey1.ComputeHash(hash7constant);

			// key3 -- We're done with SHA1-HMAC.
			byte[] key3 = new byte[24];
			Buffer.BlockCopy(hash6, 0, key3, 0, 20);
			Buffer.BlockCopy(hash8, 0, key3, 20, 4);
			byte[] hash9 = (new HMACSHA1(key2)).ComputeHash(nonce);

			// setup buffer for 3DES
			byte[] inputBuffer = new byte[72];
			for (int i = 64; i < 72; i++) {
				inputBuffer[i] = 8;
			}
			Buffer.BlockCopy(nonce, 0, inputBuffer, 0, 64);

			// generate 3DES-CBC ciphertext
			TripleDES threedescbc = TripleDES.Create();
			threedescbc.Mode = CipherMode.CBC;
			ICryptoTransform threedesenc = threedescbc.CreateEncryptor(key3, iv);
			byte[] ciph = threedesenc.TransformFinalBlock(inputBuffer, 0, 64);

			// define encryption header
			byte[] header = new byte[28] {
				28,      0,       0,    0, // 28 bytes for this header
				1,       0,       0,    0, // using CBC
				0x03,    0x66,    0,    0, // 3DES
				0x04,    0x80,    0,    0, // SHA1
				8,       0,       0,    0, // IV length
				20,      0,       0,    0, // hash length
				72,      0,       0,    0, // cipher data length
			};

			// pack everything together
			byte[] result = new byte[128];
			Buffer.BlockCopy(header, 0, result, 0, 28);
			Buffer.BlockCopy(iv, 0, result, 28, 8);
			Buffer.BlockCopy(hash9, 0, result, 36, 20);
			Buffer.BlockCopy(ciph, 0, result, 56, 72);

			// and we're done
			return Convert.ToBase64String(result);
		}
		// get the IV of a SSO challenge response for use in SolveSSOChallenge
		// where you don't know what the other party generates.
		public static string GetSSOIV(string ssoResponseString) {
			if(ssoResponseString.Length != 172) {
				throw new Exception("Input is the wrong size! Are you sure this is a SSO response?");
			}
			byte[] ssoResponse = Convert.FromBase64String(ssoResponseString);
			if(ssoResponse.Length != 128) {
				throw new Exception("The input isn't 128 bytes long... Are you sure this is a SSO response?");
			}
			byte[] result = new byte[8];
			Buffer.BlockCopy(ssoResponse, 28, result, 0, 8);
			return Convert.ToBase64String(result);
		}
	}
	// this class may be removed for any reason, it's only here so this has a reference implementation
	internal class Executable {
		public static void Main(string[] args) {
			if(args.Length == 0) {
				Console.WriteLine(
					"You need to specify the type of challenge...\n" +
					"Avaliable: MSNP6, MSNP11, MBI_KEY_OLD\n" +
					"Other features: GetSSOIV"
				);
				return;
			}
			switch(args[0]) {
				case "msnp6":
				case "MSNP6": {
					if(args.Length != 3) {
						Console.WriteLine(
							"Not enough arguments...\n" +
							"Expected private_key (Product key),\n" +
							"challenge (usually a number)"
						);
						return;
					}
					Console.WriteLine(MSNPChallenges.SolveMSNP6Challenge(args[1], args[2]));
					break;
				}
				case "msnp11":
				case "MSNP11": {
					if(args.Length != 4) {
						Console.WriteLine(
							"Not enough arguments...\n" +
							"Expected public_key (Client ID, usually begins with PROD...),\n" +
							"private_key (Product key),\n" +
							"challenge (usually a number)"
						);
						return;
					}
					Console.WriteLine(MSNPChallenges.SolveMSNP11Challenge(args[1], args[2], args[3]));
					break;
				}
				case "sso":
				case "SSO":
				case "MBI_KEY_OLD": {
					if(args.Length != 4) {
						Console.WriteLine(
							"Not enough arguments...\n" +
							"Expected binarySecret (as base64, 32 characters (24 bytes)),\n" +
							"nonce (as base64, 64 characters (48 bytes)),\n" +
							"iv (as base64, 12 characters (8 bytes))"
						);
						return;
					}
					Console.WriteLine(AuthChallenges.SolveSSOChallenge(args[1], args[2], args[3]));
					break;
				}
				case "GetSSOIV":
				case "getIV": {
					if(args.Length != 2) {
						Console.WriteLine(
							"Not enough arguments...\n" +
							"Expected SSOResponse (as base64, 172 characters (128 bytes))."
						);
						return;
					}
					Console.WriteLine(AuthChallenges.GetSSOIV(args[1]));
					break;
				}
				default: {
					Console.WriteLine(
						"Unknown mode: {0}.\n" +
						"Avaliable: MSNP6, MSNP11, MBI_KEY_OLD\n" +
						"Other features: GetSSOIV", args[0]
					);
					break;
				}
			}
			return;
		}
	}
}