| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458 | using System.Collections.Concurrent;using System.Security.Cryptography;using InABox.Core;using InABox.Database;namespace InABox.API{    public static class CredentialsCache    {        private static ConcurrentBag<User>? cache;                private static readonly User BYPASSED_USER = new() { ID = CoreUtils.FullGuid, UserID = "" };        private static void EnsureCache(bool force)        {            if (cache == null || force)            {                var _table = DbFactory.NewProvider(Logger.Main).Query(                    null,                    Columns.None<User>().Add(                        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 _decryptedUser) &&                        Encryption.Decrypt(password, "7mhvLnqMwkCAzN+zNGlyyg", out var _decryptedPass))                        if (long.TryParse(_decryptedUser, out var _userticks) && long.TryParse(_decryptedPass, 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 _exception)                {                    Logger.Send(LogType.Error, "", $"*** Unknown Error: {_exception.Message}\n{_exception.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, out var _);                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, out var _);                return null;            }            if(_session.User == CoreUtils.FullGuid)            {                return BYPASSED_USER;            }            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 BYPASSED_USER;                        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, out var _);        }        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 ConcurrentDictionary<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 _expiredkeys = sessions                .Where(x => x.Value.Expiry < DateTime.Now);            foreach (var _expiredkey in _expiredkeys)                sessions.TryRemove(_expiredkey);        }        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 _exception)            {                Logger.Send(LogType.Error, "", $"Error while saving session cache: {_exception.Message}");            }        }        public static void LoadSessionCache()        {            try            {                if (CacheFile != null)                {                    var _cachedData = Serialization.Deserialize<Dictionary<Guid, Session>>(new FileStream(CacheFile, FileMode.Open))?                        .Where(x => x.Value.Expiry != DateTime.MaxValue).ToDictionary(x => x.Key, x => x.Value);                    if (_cachedData != null)                        sessions.AddRange(_cachedData);                    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 _id = Guid.NewGuid();            sessions[_id] = new() { User = user.ID, Valid = valid, Expiry = expiry ?? (DateTime.Now + SessionExpiry), UserID = user.UserID };            return _id;        }        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 = TWO_FA_TRIES;            }        }        private static readonly ConcurrentDictionary<Guid, AuthenticationCode> authenticationCodes = new();        private static readonly int TWO_FA_TRIES = 3;        public static readonly int CODE_LENGTH = 6;        private static readonly TimeSpan EXPIRY2_FA_CODE_TIME = 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 _char = 0; _char < CODE_LENGTH; _char++)                _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 _newSession = NewSession(_user, false);            Logger.Send(LogType.Information, "", $"New login session {_newSession} 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[_newSession] = new AuthenticationCode(_code, DateTime.Now + EXPIRY2_FA_CODE_TIME);                var _recipientAddress = _user.Recipient2FA;                if (_smsProvider.SendMessage(_recipientAddress, $"Your authentication code is {_code}. This code will expire in {EXPIRY2_FA_CODE_TIME.Minutes} minutes."))                {                    Logger.Send(LogType.Information, "", "Code sent!");                    var _first = _recipientAddress[..3];                    var _last = _recipientAddress[^3..];                    recipient = _first + new string('*', _recipientAddress.Length - 6) + _last;                    return _newSession;                }                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 _newSession;            }        }        private static readonly int CODE_MODULO = (int)Math.Pow(10, CODE_LENGTH);        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.Last() & 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 % CODE_MODULO).ToString().PadLeft(CODE_LENGTH, '0');        }        private static bool CheckAuthenticationCode(byte[] token, string code)        {            var _time = DateTimeOffset.Now.ToUnixTimeSeconds();            for (long _l = _time - 30; _l <= _time; _l += 30)            {                if(GenerateGoogleAuthenticatorCode(_l, 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, out _);                    }                    _valid = false;                }                else if (_result.Expiry < DateTime.Now)                {                    authenticationCodes.Remove(sessionID, out _);                    _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    }}
 |