| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457 | 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}
 |