You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

316 lines
12 KiB

// Copyright (c) 2020, Oracle and/or its affiliates.
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License, version 2.0, as
// published by the Free Software Foundation.
//
// This program is also distributed with certain software (including
// but not limited to OpenSSL) that is licensed under separate terms,
// as designated in a particular file or component or in included license
// documentation. The authors of MySQL hereby grant you an
// additional permission to link the program and your derivative works
// with the separately licensed software that they have included with
// MySQL.
//
// Without limiting anything contained in the foregoing, this file,
// which is part of MySQL Connector/NET, is also subject to the
// Universal FOSS Exception, version 1.0, a copy of which can be found at
// http://oss.oracle.com/licenses/universal-foss-exception.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU General Public License, version 2.0, for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using Sog.Properties;
namespace MySql.Data.MySqlClient.Authentication
{
/// <summary>
/// Base class to handle SCRAM authentication methods
/// </summary>
internal abstract class ScramBase : MySqlSASLPlugin
{
/// <summary>
/// Defines the state of the authentication process.
/// </summary>
internal enum AuthState
{
INITIAL,
FINAL,
VALIDATE
}
protected string Host { get; private set; }
protected string Username { get; private set; }
protected string Password { get; private set; }
internal string _cnonce, client;
internal byte[] salted, auth;
internal AuthState _state;
protected ScramBase(string username, string password, string host)
{
this.Host = host;
this.Username = username;
this.Password = password;
}
/// <summary>
/// Gets the name of the method.
/// </summary>
internal abstract string MechanismName
{
get;
}
/// <summary>
/// Parses the server's challenge token and returns the next challenge response.
/// </summary>
/// <returns>The next challenge response.</returns>
internal byte[] Challenge(byte[] token)
{
byte[] response = null;
switch (this._state)
{
case AuthState.INITIAL:
response = this.ClientInitial();
this._state = AuthState.FINAL;
break;
case AuthState.FINAL:
response = this.ProcessServerResponse(token);
this._state = AuthState.VALIDATE;
break;
case AuthState.VALIDATE:
this.ValidateAuth(token);
break;
default:
throw new Exception("Unexpected SCRAM authentication message.");
}
return response;
}
/// <summary>
/// Builds up the client-first message.
/// </summary>
/// <returns>An array of bytes containig the client-first message.</returns>
internal byte[] ClientInitial()
{
this._cnonce = this._cnonce ?? GetRandomBytes(32);
this.client = $"n={Normalize(this.Username)},r={this._cnonce}";
return Encoding.UTF8.GetBytes($"n,a={Normalize(this.Username)}," + this.client);
}
/// <summary>
/// Processes the server response from the client-first message and
/// builds up the client-final message.
/// </summary>
/// <param name="data">Response from the server.</param>
/// <returns>An array of bytes containing the client-final message.</returns>
internal byte[] ProcessServerResponse(byte[] data)
{
string response = Encoding.UTF8.GetString(data, 0, data.Length);
var tokens = ParseServerChallenge(response);
if (!tokens.TryGetValue('s', out string salt))
{
throw new MySqlException(string.Format(Resources.AuthenticationFailed, this.Host, this.Username,
this.MechanismName, "salt is missing."));
}
if (!tokens.TryGetValue('r', out string snonce))
{
throw new MySqlException(string.Format(Resources.AuthenticationFailed, this.Host, this.Username,
this.MechanismName, "nonce is missing."));
}
if (!tokens.TryGetValue('i', out string iterations))
{
throw new MySqlException(string.Format(Resources.AuthenticationFailed, this.Host, this.Username,
this.MechanismName, "iteration count is missing."));
}
if (!tokens['r'].StartsWith(this._cnonce, StringComparison.Ordinal))
{
throw new MySqlException(string.Format(Resources.AuthenticationFailed, this.Host, this.Username,
this.MechanismName, "invalid nonce."));
}
if (!int.TryParse(iterations, out int count))
{
throw new MySqlException(string.Format(Resources.AuthenticationFailed, this.Host, this.Username,
this.MechanismName, "invalid iteration count."));
}
var password = Encoding.UTF8.GetBytes(this.Password);
this.salted = this.Hi(password, Convert.FromBase64String(salt), count);
Array.Clear(password, 0, password.Length);
var withoutProof = "c=" + Convert.ToBase64String(Encoding.ASCII.GetBytes($"n,a={this.Username},")) + ",r=" + snonce;
this.auth = Encoding.UTF8.GetBytes(this.client + "," + response + "," + withoutProof);
var ckey = this.HMAC(this.salted, Encoding.ASCII.GetBytes("Client Key"));
Xor(ckey, this.HMAC(this.Hash(ckey), this.auth));
return Encoding.UTF8.GetBytes(withoutProof + ",p=" + Convert.ToBase64String(ckey));
}
/// <summary>
/// Validates the server response.
/// </summary>
/// <param name="data">Server-final message</param>
internal void ValidateAuth(byte[] data)
{
string response = Encoding.UTF8.GetString(data, 0, data.Length);
if (!response.StartsWith("v=", StringComparison.Ordinal))
{
throw new MySqlException(string.Format(Resources.AuthenticationFailed, this.Host, this.Username, this.MechanismName, "challenge did not start with a signature."));
}
var signature = Convert.FromBase64String(response.Substring(2));
var skey = this.HMAC(this.salted, Encoding.ASCII.GetBytes("Server Key"));
var calculated = this.HMAC(skey, this.auth);
if (signature.Length != calculated.Length)
{
throw new MySqlException(string.Format(Resources.AuthenticationFailed, this.Host, this.Username, this.MechanismName, "challenge contained a signature with an invalid length."));
}
for (int i = 0; i < signature.Length; i++)
{
if (signature[i] != calculated[i])
{
throw new MySqlException(string.Format(Resources.AuthenticationFailed, this.Host, this.Username, this.MechanismName, "challenge contained an invalid signature."));
}
}
}
static string Normalize(string str)
{
var builder = new StringBuilder();
var prepared = SaslPrep(str);
for (int i = 0; i < prepared.Length; i++)
{
switch (prepared[i])
{
case ',': builder.Append("=2C"); break;
case '=': builder.Append("=3D"); break;
default:
builder.Append(prepared[i]);
break;
}
}
return builder.ToString();
}
/// <summary>
/// Creates the HMAC SHA1 context.
/// </summary>
/// <returns>The HMAC context.</returns>
/// <param name="key">The secret key.</param>
protected abstract KeyedHashAlgorithm CreateHMAC(byte[] key);
/// <summary>
/// Apply the HMAC keyed algorithm.
/// </summary>
/// <returns>The results of the HMAC keyed algorithm.</returns>
/// <param name="key">The key.</param>
/// <param name="str">The string.</param>
byte[] HMAC(byte[] key, byte[] str)
{
using (var hmac = this.CreateHMAC(key))
{
return hmac.ComputeHash(str);
}
}
/// <summary>
/// Applies the cryptographic hash function.
/// </summary>
/// <returns>The results of the hash.</returns>
/// <param name="str">The string.</param>
protected abstract byte[] Hash(byte[] str);
/// <summary>
/// Applies the exclusive-or operation to combine two octet strings.
/// </summary>
/// <param name="a">The alpha component.</param>
/// <param name="b">The blue component.</param>
private static void Xor(byte[] a, byte[] b)
{
for (int i = 0; i < a.Length; i++)
{
a[i] = (byte)(a[i] ^ b[i]);
}
}
// Hi(str, salt, i):
//
// U1 := HMACSHA1(str, salt + INT(1))
// U2 := HMACSHA1(str, U1)
// ...
// Ui-1 := HMACSHA1(str, Ui-2)
// Ui := HMACSHA1(str, Ui-1)
//
// Hi := U1 XOR U2 XOR ... XOR Ui
//
// where "i" is the iteration count, "+" is the string concatenation
// operator, and INT(g) is a 4-octet encoding of the integer g, most
// significant octet first.
//
// Hi() is, essentially, PBKDF2 [RFC2898] with HMACSHA1() as the
// pseudorandom function (PRF) and with dkLen == output length of
// HMACSHA1() == output length of H().
private byte[] Hi(byte[] str, byte[] salt, int count)
{
using (var hmac = this.CreateHMAC(str))
{
var salt1 = new byte[salt.Length + 4];
byte[] hi, u1;
Buffer.BlockCopy(salt, 0, salt1, 0, salt.Length);
salt1[salt1.Length - 1] = (byte)1;
hi = u1 = hmac.ComputeHash(salt1);
for (int i = 1; i < count; i++)
{
var u2 = hmac.ComputeHash(u1);
Xor(hi, u2);
u1 = u2;
}
return hi;
}
}
static Dictionary<char, string> ParseServerChallenge(string challenge)
{
var results = new Dictionary<char, string>();
foreach (string part in challenge.Split(','))
{
if (part[1] == '=')
{
results.Add(part[0], part.Substring(2));
}
}
return results;
}
}
}