| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459 | using System.Collections.Concurrent;using System.Security.Cryptography;using InABox.API;using InABox.Core;using InABox.Database;using Microsoft.Exchange.WebServices.Data;namespace InABox.API{    public static class CredentialsCache    {        private static ConcurrentBag<User> _cache;        private static readonly User BypassedUser = new() { ID = CoreUtils.FullGuid, UserID = "" };        private static void EnsureCache(bool force)        {            if (_cache == null || force)            {                var table = DbFactory.Provider.Query(                    null,                    new Columns<User>(                        x => x.ID,                        x => x.UserID,                        x => x.Password,                        x => x.Use2FA,                        x => x.Recipient2FA,                        x => x.TwoFactorAuthenticationType,                        x => x.AuthenticatorToken,                        x => x.PIN,                        x => x.SecurityGroup.ID,                        x => x.PasswordExpiration                    )                );                _cache = new ConcurrentBag<User>();                foreach (var row in table.Rows)                    _cache.Add(row.ToObject<User>());            }        }        public static bool IsBypassed(string userid, string password)        {            //if ((userid == "FROGSOFTWARE") && (password == "FROGSOFTWARE"))            //    return true;            if (userid.IsBase64String() && password.IsBase64String())                try                {                    if (Encryption.Decrypt(userid, "wCq9rryEJEuHIifYrxRjxg", out var sUserTicks) &&                        Encryption.Decrypt(password, "7mhvLnqMwkCAzN+zNGlyyg", out var sPassTicks))                        if (long.TryParse(sUserTicks, out var userticks) && long.TryParse(sPassTicks, out var passticks))                            if (userticks == passticks)                            {                                var remotedate = new DateTime(userticks);                                var localdate = DateTime.Now.ToUniversalTime();                                if (remotedate >= localdate.AddDays(-1) && remotedate <= localdate.AddDays(1))                                    return true;                            }                }                catch (Exception e)                {                    Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));                }            return false;        }        public static Guid Validate(Guid sessionID, out string? userID)        {            EnsureCache(false);            if(!sessions.TryGetValue(sessionID, out var session) || !session.Valid)            {                userID = null;                return Guid.Empty;            }            if(session.Expiry < DateTime.Now)            {                sessions.Remove(sessionID);                userID = null;                return Guid.Empty;            }            userID = session.UserID;            return session.User;        }        public static User? Validate(Guid sessionID)        {            EnsureCache(false);            if (!sessions.TryGetValue(sessionID, out var session) || !session.Valid)            {                return null;            }            if (session.Expiry < DateTime.Now)            {                sessions.Remove(sessionID);                return null;            }            if(session.User == CoreUtils.FullGuid)            {                return BypassedUser;            }            return _cache.FirstOrDefault(x => x.ID == session.User);        }        /// <summary>        /// Validate a given session, and refresh the session expiry if valid; use for database queries that need to refresh the user's expiry time.        /// </summary>        /// <param name="sessionID"></param>        /// <returns></returns>        public static User? ValidateAndRefresh(Guid sessionID)        {            var user = Validate(sessionID);            if(user is not null)            {                RefreshSessionExpiry(sessionID);            }            return user;        }        public static User? ValidateUser(string? pin)        {            if (String.IsNullOrWhiteSpace(pin))                return null;            EnsureCache(false);            return _cache.FirstOrDefault(x => string.Equals(x.PIN, pin));        }        public static User? ValidateUser(string? userId, string? password)        {            if (String.IsNullOrWhiteSpace(userId) || String.IsNullOrWhiteSpace(password))                return null;            if (IsBypassed(userId, password))                return BypassedUser;            EnsureCache(false);            return _cache.FirstOrDefault(x => string.Equals(x.UserID, userId) && string.Equals(x.Password, password));        }        public static void LogoutUser(Guid userGuid)        {            sessions.Remove(userGuid);        }        public static void Refresh(bool force)        {            EnsureCache(force);        }        #region Sessions        private class Session        {            public Guid User { get; init; }            public string UserID { get; init; }            public bool Valid { get; set; }            public DateTime Expiry { get; set; }        }        // SessionID => Session        private static Dictionary<Guid, Session> sessions = new();        public static TimeSpan SessionExpiry = TimeSpan.FromHours(8);        public static string? CacheFile { get; set; }        public static IEnumerable<Guid> GetUserSessions(Guid userID)        {            return sessions.Where(x => x.Value.User == userID).Select(x => x.Key);        }        private static void CheckSessionExpiries()        {            var now = DateTime.Now;            sessions = sessions                .Where(x => x.Value.Expiry >= now)                .ToDictionary(x => x.Key, x => x.Value);        }        public static void SetSessionExpiryTime(TimeSpan expiry)        {            SessionExpiry = expiry;        }        public static void RefreshSessionExpiry(Guid sessionID)        {            if (sessions.TryGetValue(sessionID, out var session))            {                if (session.Expiry != DateTime.MaxValue)                {                    session.Expiry = DateTime.Now + SessionExpiry;                }            }        }        public static void SaveSessionCache()        {            CheckSessionExpiries();            try            {                if (CacheFile != null)                {                    var json = Serialization.Serialize(sessions.Where(x => x.Value.Expiry != DateTime.MaxValue).ToDictionary(x => x.Key, x => x.Value));                    File.WriteAllText(CacheFile, json);                }                else                {                    Logger.Send(LogType.Error, "", "Error while saving session cache: No Cache file set!");                }            }            catch (Exception e)            {                Logger.Send(LogType.Error, "", $"Error while saving session cache: {e.Message}");            }        }        public static void LoadSessionCache()        {            try            {                if (CacheFile != null)                {                    sessions = Serialization.Deserialize<Dictionary<Guid, Session>>(new FileStream(CacheFile, FileMode.Open))                        .Where(x => x.Value.Expiry != DateTime.MaxValue).ToDictionary(x => x.Key, x => x.Value);                    CheckSessionExpiries();                }                else                {                    sessions = new();                }            }            catch (Exception)            {                sessions = new();            }        }        public static void SetCacheFile(string cacheFile)        {            CacheFile = cacheFile;        }        public static Guid NewSession(User user, bool valid = true, DateTime? expiry = null)        {            var session = Guid.NewGuid();            sessions[session] = new() { User = user.ID, Valid = valid, Expiry = expiry ?? (DateTime.Now + SessionExpiry), UserID = user.UserID };            return session;        }        public static bool SessionExists(Guid session)        {            return sessions.ContainsKey(session);        }        #endregion        #region 2FA        private class AuthenticationCode        {            public string Code { get; set; }            public DateTime Expiry { get; set; }            public int TriesLeft { get; set; }            public AuthenticationCode(string code, DateTime expiry)            {                Code = code;                Expiry = expiry;                TriesLeft = TwoFATries;            }        }        private static Dictionary<Guid, AuthenticationCode> authenticationCodes = new();        private static readonly int TwoFATries = 3;        public static readonly int CodeLength = 6;        private static readonly TimeSpan Expiry2FACodeTime = TimeSpan.FromMinutes(15);        private static Dictionary<SMSProviderType, ISMSProvider> SMSProviders { get; set; } = new();        public static void AddSMSProvider(ISMSProvider provider)         {            SMSProviders.Add(provider.ProviderType, provider);        }        private static string GenerateCode()        {            var random = new Random(DateTime.Now.Millisecond);            var code = "";            for (int i = 0; i < CodeLength; i++)            {                code += random.Next(10).ToString();            }            return code;        }        public static Guid? SendCode(Guid userGuid, out string? recipient)        {            EnsureCache(false);            var user = _cache.FirstOrDefault(x => x.ID == userGuid);            if(user == null)            {                Logger.Send(LogType.Error, "", "Cannot send code; user does not exist!");                recipient = null;                return null;            }            var session = NewSession(user, false);            Logger.Send(LogType.Information, "", $"New login session {session} for {user.UserID}");            if (user.TwoFactorAuthenticationType != TwoFactorAuthenticationType.GoogleAuthenticator)            {                var smsProvider = SMSProviders                    .Where(x => x.Value.TwoFactorAuthenticationType == user.TwoFactorAuthenticationType)                    .Select(x => x.Value).FirstOrDefault();                if (smsProvider == null)                {                    Logger.Send(LogType.Error, "", "Cannot send code; user requests a 2FA method which is not supported!");                    recipient = null;                    return null;                }                var code = GenerateCode();                Logger.Send(LogType.Information, "", $"Code for session {userGuid} is {code}");                authenticationCodes.Add(session, new AuthenticationCode(code, DateTime.Now + Expiry2FACodeTime));                var recAddr = user.Recipient2FA;                if (smsProvider.SendMessage(recAddr, $"Your authentication code is {code}. This code will expire in {Expiry2FACodeTime.Minutes} minutes."))                {                    Logger.Send(LogType.Information, "", "Code sent!");                    var first = recAddr[..3];                    var last = recAddr[^3..];                    recipient = first + new string('*', recAddr.Length - 6) + last;                    return session;                }                else                {                    Logger.Send(LogType.Information, "", "Code failed to send!");                    recipient = null;                    return null;                }            }            else            {                Logger.Send(LogType.Information, "", $"Google authenticator is being used");                recipient = "Google Authenticator";                return session;            }        }        private static readonly int CodeModulo = (int)Math.Pow(10, CodeLength);        private static string GenerateGoogleAuthenticatorCode(long time, byte[] key)        {            var window = time / 30;            var hex = window.ToString("x");            if (hex.Length < 16)            {                hex = hex.PadLeft(16, '0');            }            var bytes = Convert.FromHexString(hex);            var hash = new HMACSHA1(key).ComputeHash(bytes);            var offset = hash[hash.Length - 1] & 0xf;            var selected = new byte[4];            Buffer.BlockCopy(hash, offset, selected, 0, 4);            if (BitConverter.IsLittleEndian)            {                Array.Reverse(selected);            }            var integer = BitConverter.ToInt32(selected, 0);            var truncated = integer & 0x7fffffff;            return (truncated % CodeModulo).ToString().PadLeft(CodeLength, '0');        }        private static bool CheckAuthenticationCode(byte[] token, string code)        {            var time = DateTimeOffset.Now.ToUnixTimeSeconds();            for (long i = time - 30; i <= time; i += 30)            {                if(GenerateGoogleAuthenticatorCode(i, token) == code)                {                    return true;                }            }            return false;        }        public static bool ValidateCode(Guid sessionID, string code)        {            if (!sessions.TryGetValue(sessionID, out var session))            {                return false;            }            bool valid;            if(authenticationCodes.TryGetValue(sessionID, out var result))            {                if (result.Code != code)                {                    result.TriesLeft--;                    if (result.TriesLeft == 0)                    {                        authenticationCodes.Remove(sessionID);                    }                    valid = false;                }                else if (result.Expiry < DateTime.Now)                {                    authenticationCodes.Remove(sessionID);                    valid = false;                }                else                {                    valid = true;                }            }            else            {                var user = _cache.FirstOrDefault(x => x.ID == session.User);                if(user?.TwoFactorAuthenticationType == TwoFactorAuthenticationType.GoogleAuthenticator)                {                    valid = CheckAuthenticationCode(user.AuthenticatorToken, code);                }                else                {                    valid = false;                }            }            if (valid)            {                session.Valid = true;                return true;            }            else            {                return false;            }        }        #endregion    }}
 |