/// <summary> /// Parses the server's challenge token and returns the next challenge response. /// </summary> /// <remarks> /// Parses the server's challenge token and returns the next challenge response. /// </remarks> /// <returns>The next challenge response.</returns> /// <param name="token">The server's challenge token.</param> /// <param name="startIndex">The index into the token specifying where the server's challenge begins.</param> /// <param name="length">The length of the server's challenge.</param> /// <exception cref="System.InvalidOperationException"> /// The SASL mechanism is already authenticated. /// </exception> /// <exception cref="SaslException"> /// An error has occurred while parsing the server's challenge token. /// </exception> protected override byte[] Challenge(byte[] token, int startIndex, int length) { if (IsAuthenticated) { throw new InvalidOperationException(); } var cred = Credentials.GetCredential(Uri, MechanismName); byte[] response, signature; switch (state) { case LoginState.Initial: if (string.IsNullOrEmpty(cnonce)) { var entropy = new byte[18]; using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(entropy); cnonce = Convert.ToBase64String(entropy); } client = "n=" + Normalize(cred.UserName) + ",r=" + cnonce; response = Encoding.UTF8.GetBytes("n,," + client); state = LoginState.Final; break; case LoginState.Final: server = Encoding.UTF8.GetString(token, startIndex, length); var tokens = ParseServerChallenge(server); string salt, nonce, iterations; int count; if (!tokens.TryGetValue('s', out salt)) { throw new SaslException(MechanismName, SaslErrorCode.IncompleteChallenge, "Challenge did not contain a salt."); } if (!tokens.TryGetValue('r', out nonce)) { throw new SaslException(MechanismName, SaslErrorCode.IncompleteChallenge, "Challenge did not contain a nonce."); } if (!tokens.TryGetValue('i', out iterations)) { throw new SaslException(MechanismName, SaslErrorCode.IncompleteChallenge, "Challenge did not contain an iteration count."); } if (!nonce.StartsWith(cnonce, StringComparison.Ordinal)) { throw new SaslException(MechanismName, SaslErrorCode.InvalidChallenge, "Challenge contained an invalid nonce."); } if (!int.TryParse(iterations, out count) || count < 1) { throw new SaslException(MechanismName, SaslErrorCode.InvalidChallenge, "Challenge contained an invalid iteration count."); } var password = Encoding.UTF8.GetBytes(SaslPrep(cred.Password)); salted = Hi(password, Convert.FromBase64String(salt), count); var withoutProof = "c=" + Convert.ToBase64String(Encoding.ASCII.GetBytes("n,,")) + ",r=" + nonce; auth = Encoding.UTF8.GetBytes(client + "," + server + "," + withoutProof); var key = HMAC(salted, Encoding.ASCII.GetBytes("Client Key")); signature = HMAC(Hash(key), auth); Xor(key, signature); response = Encoding.UTF8.GetBytes(withoutProof + ",p=" + Convert.ToBase64String(key)); state = LoginState.Validate; break; case LoginState.Validate: var challenge = Encoding.UTF8.GetString(token, startIndex, length); if (!challenge.StartsWith("v=", StringComparison.Ordinal)) { throw new SaslException(MechanismName, SaslErrorCode.InvalidChallenge, "Challenge did not start with a signature."); } signature = Convert.FromBase64String(challenge.Substring(2)); var serverKey = HMAC(salted, Encoding.ASCII.GetBytes("Server Key")); var calculated = HMAC(serverKey, auth); if (signature.Length != calculated.Length) { throw new SaslException(MechanismName, SaslErrorCode.IncorrectHash, "Challenge contained a signature with an invalid length."); } for (int i = 0; i < signature.Length; i++) { if (signature[i] != calculated[i]) { throw new SaslException(MechanismName, SaslErrorCode.IncorrectHash, "Challenge contained an invalid signatire."); } } IsAuthenticated = true; response = new byte[0]; break; default: throw new IndexOutOfRangeException("state"); } return(response); }
/// <summary> /// Parses the server's challenge token and returns the next challenge response. /// </summary> /// <remarks> /// Parses the server's challenge token and returns the next challenge response. /// </remarks> /// <returns>The next challenge response.</returns> /// <param name="token">The server's challenge token.</param> /// <param name="startIndex">The index into the token specifying where the server's challenge begins.</param> /// <param name="length">The length of the server's challenge.</param> /// <exception cref="System.InvalidOperationException"> /// The SASL mechanism is already authenticated. /// </exception> /// <exception cref="System.NotSupportedException"> /// THe SASL mechanism does not support SASL-IR. /// </exception> /// <exception cref="SaslException"> /// An error has occurred while parsing the server's challenge token. /// </exception> protected override byte[] Challenge(byte[] token, int startIndex, int length) { if (IsAuthenticated) { throw new InvalidOperationException(); } if (token == null) { throw new NotSupportedException("DIGEST-MD5 does not support SASL-IR."); } var cred = Credentials.GetCredential(Uri, MechanismName); switch (state) { case LoginState.Auth: if (token.Length > 2048) { throw new SaslException(MechanismName, SaslErrorCode.ChallengeTooLong, "Server challenge too long."); } challenge = DigestChallenge.Parse(Encoding.UTF8.GetString(token, startIndex, length)); if (string.IsNullOrEmpty(cnonce)) { var entropy = new byte[15]; using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(entropy); cnonce = Convert.ToBase64String(entropy); } response = new DigestResponse(challenge, Uri.Scheme, Uri.DnsSafeHost, cred.UserName, cred.Password, cnonce); state = LoginState.Final; return(response.Encode()); case LoginState.Final: if (token.Length == 0) { throw new SaslException(MechanismName, SaslErrorCode.MissingChallenge, "Server response did not contain any authentication data."); } var text = Encoding.UTF8.GetString(token, startIndex, length); string key, value; int index = 0; if (!DigestChallenge.TryParseKeyValuePair(text, ref index, out key, out value)) { throw new SaslException(MechanismName, SaslErrorCode.IncompleteChallenge, "Server response contained incomplete authentication data."); } var expected = response.ComputeHash(cred.Password, false); if (value != expected) { throw new SaslException(MechanismName, SaslErrorCode.IncorrectHash, "Server response did not contain the expected hash."); } IsAuthenticated = true; return(new byte[0]); default: throw new IndexOutOfRangeException("state"); } }