/* Copyright (C) 2014-2017 Tal Aloni <tal.aloni.il@gmail.com>. All rights reserved.
 * 
 * You can redistribute this program and/or modify it under the terms of
 * the GNU Lesser Public License as published by the Free Software Foundation,
 * either version 3 of the License, or (at your option) any later version.
 */
using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using Utilities;
using SMBLibrary.Authentication;
using SMBLibrary.Win32.Security;
using Microsoft.Win32;

namespace SMBLibrary.Server.Win32
{
    public class Win32UserCollection : UserCollection, INTLMAuthenticationProvider
    {
        private SecHandle m_serverContext;
        private byte[] m_serverChallenge = new byte[8];

        public Win32UserCollection()
        {
            List<string> users = NetworkAPI.EnumerateNetworkUsers();
            foreach (string user in users)
            {
                this.Add(new User(user, String.Empty));
            }
        }

        public ChallengeMessage GetChallengeMessage(NegotiateMessage negotiateMessage)
        {
            byte[] negotiateMessageBytes = negotiateMessage.GetBytes();
            byte[] challengeMessageBytes = SSPIHelper.GetType2Message(negotiateMessageBytes, out m_serverContext);
            ChallengeMessage challengeMessage = new ChallengeMessage(challengeMessageBytes);
            m_serverChallenge = challengeMessage.ServerChallenge;
            return challengeMessage;
        }

        /// <summary>
        /// Authenticate will return false when the password is correct in these cases:
        /// 1. The correct password is blank and 'limitblankpassworduse' is set to 1.
        /// 2. The user is listed in the "Deny access to this computer from the network" list.
        /// </summary>
        public bool Authenticate(AuthenticateMessage message)
        {
            if ((message.NegotiateFlags & NegotiateFlags.Anonymous) > 0)
            {
                return this.EnableGuestLogin;
            }

            // AuthenticateType3Message is not reliable when 'limitblankpassworduse' is set to 1 and the user has an empty password set.
            // Note: Windows LogonUser API calls will be listed in the security event log.
            if (!AreEmptyPasswordsAllowed() &&
                IsPasswordEmpty(message) &&
                LoginAPI.HasEmptyPassword(message.UserName))
            {
                if (FallbackToGuest(message.UserName))
                {
                    return false;
                }
                else
                {
                    throw new EmptyPasswordNotAllowedException();
                }
            }

            byte[] messageBytes = message.GetBytes();
            try
            {
                return SSPIHelper.AuthenticateType3Message(m_serverContext, messageBytes);
            }
            catch (Exception)
            {
                return false;
            }
        }

        public bool IsPasswordEmpty(AuthenticateMessage message)
        {
            // See [MS-NLMP] 3.3.1 - NTLM v1 Authentication
            // Special case for anonymous authentication:
            if (message.LmChallengeResponse.Length == 1 || message.NtChallengeResponse.Length == 0)
            {
                return true;
            }

            if ((message.NegotiateFlags & NegotiateFlags.ExtendedSecurity) > 0)
            {
                if (AuthenticationMessageUtils.IsNTLMv1ExtendedSecurity(message.LmChallengeResponse))
                {
                    // NTLM v1 extended security:
                    byte[] clientChallenge = ByteReader.ReadBytes(message.LmChallengeResponse, 0, 8);
                    byte[] emptyPasswordNTLMv1Response = NTLMCryptography.ComputeNTLMv1ExtendedSecurityResponse(m_serverChallenge, clientChallenge, String.Empty);
                    if (ByteUtils.AreByteArraysEqual(emptyPasswordNTLMv1Response, message.NtChallengeResponse))
                    {
                        return true;
                    }
                }
                else
                {
                    // NTLM v2:
                    byte[] _LMv2ClientChallenge = ByteReader.ReadBytes(message.LmChallengeResponse, 16, 8);
                    byte[] emptyPasswordLMv2Response = NTLMCryptography.ComputeLMv2Response(m_serverChallenge, _LMv2ClientChallenge, String.Empty, message.UserName, message.DomainName);
                    if (ByteUtils.AreByteArraysEqual(emptyPasswordLMv2Response, message.LmChallengeResponse))
                    {
                        return true;
                    }

                    if (AuthenticationMessageUtils.IsNTLMv2NTResponse(message.NtChallengeResponse))
                    {
                        byte[] clientNTProof = ByteReader.ReadBytes(message.NtChallengeResponse, 0, 16);
                        byte[] clientChallengeStructurePadded = ByteReader.ReadBytes(message.NtChallengeResponse, 16, message.NtChallengeResponse.Length - 16);
                        byte[] emptyPasswordNTProof = NTLMCryptography.ComputeNTLMv2Proof(m_serverChallenge, clientChallengeStructurePadded, String.Empty, message.UserName, message.DomainName);
                        if (ByteUtils.AreByteArraysEqual(clientNTProof, emptyPasswordNTProof))
                        {
                            return true;
                        }
                    }
                }
            }
            else
            {
                // NTLM v1:
                byte[] emptyPasswordLMv1Response = NTLMCryptography.ComputeLMv1Response(m_serverChallenge, String.Empty);
                if (ByteUtils.AreByteArraysEqual(emptyPasswordLMv1Response, message.LmChallengeResponse))
                {
                    return true;
                }

                byte[] emptyPasswordNTLMv1Response = NTLMCryptography.ComputeNTLMv1Response(m_serverChallenge, String.Empty);
                if (ByteUtils.AreByteArraysEqual(emptyPasswordNTLMv1Response, message.NtChallengeResponse))
                {
                    return true;
                }
            }

            return false;
        }

        public bool FallbackToGuest(string userName)
        {
            return (EnableGuestLogin && (IndexOf(userName) == -1));
        }

        /// <summary>
        /// We immitate Windows, Guest logins are disabled in any of these cases:
        /// 1. The Guest account is disabled.
        /// 2. The Guest account has password set.
        /// 3. The Guest account is listed in the "deny access to this computer from the network" list.
        /// </summary>
        private bool EnableGuestLogin
        {
            get
            {
                return LoginAPI.ValidateUserPassword("Guest", String.Empty, LogonType.Network);
            }
        }

        public static bool AreEmptyPasswordsAllowed()
        {
            RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Control\Lsa");
            object value = key.GetValue("limitblankpassworduse", 1);
            if (value is int)
            {
                if ((int)value != 0)
                {
                    return false;
                }
            }
            return true;
        }
    }
}