using System; using System.Collections.Specialized; using System.Linq; using System.Security.Cryptography; using System.Text; namespace S22.Sasl.Mechanisms { /// /// Implements the Sasl SCRAM-SHA-1 authentication method as described in /// RFC 5802. /// internal class SaslScramSha1 : SaslMechanism { bool Completed = false; /// /// The client nonce value used during authentication. /// string Cnonce = GenerateCnonce(); /// /// Scram-Sha-1 involves several steps. /// int Step = 0; /// /// The salted password. This is needed for client authentication and later /// on again for verifying the server signature. /// byte[] SaltedPassword; /// /// The auth message is part of the authentication exchange and is needed for /// authentication as well as for verifying the server signature. /// string AuthMessage; /// /// Client sends the first message in the authentication exchange. /// public override bool HasInitial { get { return true; } } /// /// True if the authentication exchange between client and server /// has been completed. /// public override bool IsCompleted { get { return Completed; } } /// /// The IANA name for the Scram-Sha-1 authentication mechanism as described /// in RFC 5802. /// public override string Name { get { return "SCRAM-SHA-1"; } } /// /// The username to authenticate with. /// string Username { get { return Properties.ContainsKey("Username") ? Properties["Username"] as string : null; } set { Properties["Username"] = value; } } /// /// The password to authenticate with. /// string Password { get { return Properties.ContainsKey("Password") ? Properties["Password"] as string : null; } set { Properties["Password"] = value; } } /// /// Private constructor for use with Sasl.SaslFactory. /// private SaslScramSha1() { // Nothing to do here. } /// /// Internal constructor used for unit testing. /// /// The username to authenticate with. /// The plaintext password to authenticate /// with. /// The client nonce value to use. /// Thrown if the username /// or the password parameter is null. /// Thrown if the username /// parameter is empty. internal SaslScramSha1(string username, string password, string cnonce) : this(username, password) { Cnonce = cnonce; } /// /// Creates and initializes a new instance of the SaslScramSha1 /// class using the specified username and password. /// /// The username to authenticate with. /// The plaintext password to authenticate /// with. /// Thrown if the username /// or the password parameter is null. /// Thrown if the username /// parameter is empty. public SaslScramSha1(string username, string password) { username.ThrowIfNull("username"); if (username == String.Empty) throw new ArgumentException("The username must not be empty."); password.ThrowIfNull("password"); Username = username; Password = password; } /// /// Computes the client response to the specified SCRAM-SHA-1 challenge. /// /// The challenge sent by the server /// The response to the SCRAM-SHA-1 challenge. /// Thrown if the response could not /// be computed. protected override byte[] ComputeResponse(byte[] challenge) { // Precondition: Ensure username and password are not null and // username is not empty. if (String.IsNullOrEmpty(Username) || Password == null) { throw new SaslException("The username must not be null or empty and " + "the password must not be null."); } if (Step == 2) Completed = true; byte[] ret = Step == 0 ? ComputeInitialResponse() : (Step == 1 ? ComputeFinalResponse(challenge) : VerifyServerSignature(challenge)); Step = Step + 1; return ret; } /// /// Computes the initial response sent by the client to the server. /// /// An array of bytes containing the initial client /// response. private byte[] ComputeInitialResponse() { // We don't support channel binding. return Encoding.UTF8.GetBytes("n,,n=" + SaslPrep(Username) + ",r=" + Cnonce); } /// /// Computes the "client-final-message" which completes the authentication /// process. /// /// The "server-first-message" challenge received /// from the server in response to the initial client response. /// An array of bytes containing the client's challenge /// response. private byte[] ComputeFinalResponse(byte[] challenge) { NameValueCollection nv = ParseServerFirstMessage(challenge); // Extract the server data needed to calculate the client proof. string salt = nv["s"], nonce = nv["r"]; int iterationCount = Int32.Parse(nv["i"]); if (!VerifyServerNonce(nonce)) throw new SaslException("Invalid server nonce: " + nonce); // Calculate the client proof (refer to RFC 5802, p.7). string clientFirstBare = "n=" + SaslPrep(Username) + ",r=" + Cnonce, serverFirstMessage = Encoding.UTF8.GetString(challenge), withoutProof = "c=" + Convert.ToBase64String(Encoding.UTF8.GetBytes("n,,")) + ",r=" + nonce; AuthMessage = clientFirstBare + "," + serverFirstMessage + "," + withoutProof; SaltedPassword = Hi(Password, salt, iterationCount); byte[] clientKey = HMAC(SaltedPassword, "Client Key"), storedKey = H(clientKey), clientSignature = HMAC(storedKey, AuthMessage), clientProof = Xor(clientKey, clientSignature); // Return the client final message. return Encoding.UTF8.GetBytes(withoutProof + ",p=" + Convert.ToBase64String(clientProof)); } /// /// Verifies the nonce value sent by the server. /// /// The nonce value sent by the server as part of the /// server-first-message. /// True if the nonce value is valid, otherwise false. bool VerifyServerNonce(string nonce) { // The first part of the server nonce must be the nonce sent by the // client in its initial response. return nonce.StartsWith(Cnonce); } /// /// Verifies the server signature which is sent by the server as the final /// step of the authentication process. /// /// The server signature as a base64-encoded /// string. /// The client's response to the server. This will be an empty /// byte array if verification was successful, or the '*' SASL cancellation /// token. private byte[] VerifyServerSignature(byte[] challenge) { string s = Encoding.UTF8.GetString(challenge); // The server must respond with a "v=signature" message. if (!s.StartsWith("v=")) { // Cancel authentication process. return Encoding.UTF8.GetBytes("*"); } byte[] serverSignature = Convert.FromBase64String(s.Substring(2)); // Verify server's signature. byte[] serverKey = HMAC(SaltedPassword, "Server Key"), calculatedSignature = HMAC(serverKey, AuthMessage); // If both signatures are equal, server has been authenticated. Otherwise // cancel the authentication process. return serverSignature.SequenceEqual(calculatedSignature) ? new byte[0] : Encoding.UTF8.GetBytes("*"); } /// /// Parses the "server-first-message" received from the server. /// /// The challenge received from the server. /// A collection of key/value pairs contained extracted from /// the server message. /// Thrown if the message parameter /// is null. private NameValueCollection ParseServerFirstMessage(byte[] challenge) { challenge.ThrowIfNull("challenge"); string message = Encoding.UTF8.GetString(challenge); NameValueCollection coll = new NameValueCollection(); foreach (string s in message.Split(',')) { int delimiter = s.IndexOf('='); if (delimiter < 0) continue; string name = s.Substring(0, delimiter), value = s.Substring(delimiter + 1); coll.Add(name, value); } return coll; } /// /// Computes the "Hi()"-formula which is part of the client's response /// to the server challenge. /// /// The supplied password to use. /// The salt received from the server. /// The iteration count. /// An array of bytes containing the result of the /// computation of the "Hi()"-formula. /// Hi is, essentially, PBKDF2 with HMAC as the /// pseudorandom function (PRF) and with dkLen == output length of /// HMAC() == output length of H(). (Refer to RFC 5802, p.6) private byte[] Hi(string password, string salt, int count) { // The salt is sent by the server as a base64-encoded string. byte[] saltBytes = Convert.FromBase64String(salt); using (var db = new Rfc2898DeriveBytes(password, saltBytes, count)) { // Generate 20 key bytes, which is the size of the hash result of SHA-1. return db.GetBytes(20); } } /// /// Applies the HMAC keyed hash algorithm using the specified key to /// the specified input data. /// /// The key to use for initializing the HMAC /// provider. /// The input to compute the hashcode for. /// The hashcode of the specified data input. private byte[] HMAC(byte[] key, byte[] data) { using (var hmac = new HMACSHA1(key)) { return hmac.ComputeHash(data); } } /// /// Applies the HMAC keyed hash algorithm using the specified key to /// the specified input string. /// /// The key to use for initializing the HMAC /// provider. /// The input string to compute the hashcode for. /// The hashcode of the specified string. private byte[] HMAC(byte[] key, string data) { return HMAC(key, Encoding.UTF8.GetBytes(data)); } /// /// Applies the cryptographic hash function SHA-1 to the specified data /// array. /// /// The data array to apply the hash function to. /// The hash value for the specified byte array. private byte[] H(byte[] data) { using (var sha1 = new SHA1Managed()) { return sha1.ComputeHash(data); } } /// /// Applies the exclusive-or operation to combine the specified byte array /// a with the specified byte array b. /// /// The first byte array. /// The second byte array. /// An array of bytes of the same length as the input arrays /// containing the result of the exclusive-or operation. /// Thrown if either argument is /// null. /// Thrown if the input arrays /// are not of the same length. private byte[] Xor(byte[] a, byte[] b) { a.ThrowIfNull("a"); b.ThrowIfNull("b"); if (a.Length != b.Length) throw new InvalidOperationException(); byte[] ret = new byte[a.Length]; for (int i = 0; i < a.Length; i++) { ret[i] = (byte)(a[i] ^ b[i]); } return ret; } /// /// Generates a random cnonce-value which is a "client-specified data string /// which must be different each time a digest-response is sent". /// /// A random "cnonce-value" string. private static string GenerateCnonce() { return Guid.NewGuid().ToString("N").Substring(0, 16); } /// /// Prepares the specified string as is described in RFC 5802. /// /// A string value. /// A "Saslprepped" string. private static string SaslPrep(string s) { // Fixme: Do this properly? return s .Replace("=", "=3D") .Replace(",", "=2C"); } } }