CredentialsCache.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. using System.Collections.Concurrent;
  2. using System.Security.Cryptography;
  3. using InABox.API;
  4. using InABox.Core;
  5. using InABox.Database;
  6. using Microsoft.Exchange.WebServices.Data;
  7. namespace InABox.API
  8. {
  9. public static class CredentialsCache
  10. {
  11. private static ConcurrentBag<User> _cache;
  12. private static readonly User BypassedUser = new() { ID = CoreUtils.FullGuid, UserID = "" };
  13. private static void EnsureCache(bool force)
  14. {
  15. if (_cache == null || force)
  16. {
  17. var table = DbFactory.NewProvider(Logger.Main).Query(
  18. null,
  19. Columns.None<User>().Add(
  20. x => x.ID,
  21. x => x.UserID,
  22. x => x.Password,
  23. x => x.Use2FA,
  24. x => x.Recipient2FA,
  25. x => x.TwoFactorAuthenticationType,
  26. x => x.AuthenticatorToken,
  27. x => x.PIN,
  28. x => x.SecurityGroup.ID,
  29. x => x.PasswordExpiration
  30. )
  31. );
  32. _cache = new ConcurrentBag<User>();
  33. foreach (var row in table.Rows)
  34. _cache.Add(row.ToObject<User>());
  35. }
  36. }
  37. public static bool IsBypassed(string userid, string password)
  38. {
  39. //if ((userid == "FROGSOFTWARE") && (password == "FROGSOFTWARE"))
  40. // return true;
  41. if (userid.IsBase64String() && password.IsBase64String())
  42. try
  43. {
  44. if (Encryption.Decrypt(userid, "wCq9rryEJEuHIifYrxRjxg", out var sUserTicks) &&
  45. Encryption.Decrypt(password, "7mhvLnqMwkCAzN+zNGlyyg", out var sPassTicks))
  46. if (long.TryParse(sUserTicks, out var userticks) && long.TryParse(sPassTicks, out var passticks))
  47. if (userticks == passticks)
  48. {
  49. var remotedate = new DateTime(userticks);
  50. var localdate = DateTime.Now.ToUniversalTime();
  51. if (remotedate >= localdate.AddDays(-1) && remotedate <= localdate.AddDays(1))
  52. return true;
  53. }
  54. }
  55. catch (Exception e)
  56. {
  57. Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
  58. }
  59. return false;
  60. }
  61. public static Guid Validate(Guid sessionID, out string? userID)
  62. {
  63. EnsureCache(false);
  64. if(!sessions.TryGetValue(sessionID, out var session) || !session.Valid)
  65. {
  66. userID = null;
  67. return Guid.Empty;
  68. }
  69. if(session.Expiry < DateTime.Now)
  70. {
  71. sessions.Remove(sessionID);
  72. userID = null;
  73. return Guid.Empty;
  74. }
  75. userID = session.UserID;
  76. return session.User;
  77. }
  78. public static User? Validate(Guid sessionID)
  79. {
  80. EnsureCache(false);
  81. if (!sessions.TryGetValue(sessionID, out var session) || !session.Valid)
  82. {
  83. return null;
  84. }
  85. if (session.Expiry < DateTime.Now)
  86. {
  87. sessions.Remove(sessionID);
  88. return null;
  89. }
  90. if(session.User == CoreUtils.FullGuid)
  91. {
  92. return BypassedUser;
  93. }
  94. return _cache.FirstOrDefault(x => x.ID == session.User);
  95. }
  96. /// <summary>
  97. /// Validate a given session, and refresh the session expiry if valid; use for database queries that need to refresh the user's expiry time.
  98. /// </summary>
  99. /// <param name="sessionID"></param>
  100. /// <returns></returns>
  101. public static User? ValidateAndRefresh(Guid sessionID)
  102. {
  103. var user = Validate(sessionID);
  104. if(user is not null)
  105. {
  106. RefreshSessionExpiry(sessionID);
  107. }
  108. return user;
  109. }
  110. public static User? ValidateUser(string? pin)
  111. {
  112. if (String.IsNullOrWhiteSpace(pin))
  113. return null;
  114. EnsureCache(false);
  115. return _cache.FirstOrDefault(x => string.Equals(x.PIN, pin));
  116. }
  117. public static User? ValidateUser(string? userId, string? password)
  118. {
  119. if (String.IsNullOrWhiteSpace(userId) || String.IsNullOrWhiteSpace(password))
  120. return null;
  121. if (IsBypassed(userId, password))
  122. return BypassedUser;
  123. EnsureCache(false);
  124. return _cache.FirstOrDefault(x => string.Equals(x.UserID, userId) && string.Equals(x.Password, password));
  125. }
  126. public static void LogoutUser(Guid userGuid)
  127. {
  128. sessions.Remove(userGuid);
  129. }
  130. public static void Refresh(bool force)
  131. {
  132. EnsureCache(force);
  133. }
  134. #region Sessions
  135. private class Session
  136. {
  137. public Guid User { get; init; }
  138. public string UserID { get; init; }
  139. public bool Valid { get; set; }
  140. public DateTime Expiry { get; set; }
  141. }
  142. // SessionID => Session
  143. private static Dictionary<Guid, Session> sessions = new();
  144. public static TimeSpan SessionExpiry = TimeSpan.FromHours(8);
  145. public static string? CacheFile { get; set; }
  146. public static IEnumerable<Guid> GetUserSessions(Guid userID)
  147. {
  148. return sessions.Where(x => x.Value.User == userID).Select(x => x.Key);
  149. }
  150. private static void CheckSessionExpiries()
  151. {
  152. var now = DateTime.Now;
  153. sessions = sessions
  154. .Where(x => x.Value.Expiry >= now)
  155. .ToDictionary(x => x.Key, x => x.Value);
  156. }
  157. public static void SetSessionExpiryTime(TimeSpan expiry)
  158. {
  159. SessionExpiry = expiry;
  160. }
  161. public static void RefreshSessionExpiry(Guid sessionID)
  162. {
  163. if (sessions.TryGetValue(sessionID, out var session))
  164. {
  165. if (session.Expiry != DateTime.MaxValue)
  166. {
  167. session.Expiry = DateTime.Now + SessionExpiry;
  168. }
  169. }
  170. }
  171. public static void SaveSessionCache()
  172. {
  173. CheckSessionExpiries();
  174. try
  175. {
  176. if (CacheFile != null)
  177. {
  178. var json = Serialization.Serialize(sessions.Where(x => x.Value.Expiry != DateTime.MaxValue).ToDictionary(x => x.Key, x => x.Value));
  179. File.WriteAllText(CacheFile, json);
  180. }
  181. else
  182. {
  183. Logger.Send(LogType.Error, "", "Error while saving session cache: No Cache file set!");
  184. }
  185. }
  186. catch (Exception e)
  187. {
  188. Logger.Send(LogType.Error, "", $"Error while saving session cache: {e.Message}");
  189. }
  190. }
  191. public static void LoadSessionCache()
  192. {
  193. try
  194. {
  195. if (CacheFile != null)
  196. {
  197. sessions = Serialization.Deserialize<Dictionary<Guid, Session>>(new FileStream(CacheFile, FileMode.Open))
  198. .Where(x => x.Value.Expiry != DateTime.MaxValue).ToDictionary(x => x.Key, x => x.Value);
  199. CheckSessionExpiries();
  200. }
  201. else
  202. {
  203. sessions = new();
  204. }
  205. }
  206. catch (Exception)
  207. {
  208. sessions = new();
  209. }
  210. }
  211. public static void SetCacheFile(string cacheFile)
  212. {
  213. CacheFile = cacheFile;
  214. }
  215. public static Guid NewSession(User user, bool valid = true, DateTime? expiry = null)
  216. {
  217. var session = Guid.NewGuid();
  218. sessions[session] = new() { User = user.ID, Valid = valid, Expiry = expiry ?? (DateTime.Now + SessionExpiry), UserID = user.UserID };
  219. return session;
  220. }
  221. public static bool SessionExists(Guid session)
  222. {
  223. return sessions.ContainsKey(session);
  224. }
  225. #endregion
  226. #region 2FA
  227. private class AuthenticationCode
  228. {
  229. public string Code { get; set; }
  230. public DateTime Expiry { get; set; }
  231. public int TriesLeft { get; set; }
  232. public AuthenticationCode(string code, DateTime expiry)
  233. {
  234. Code = code;
  235. Expiry = expiry;
  236. TriesLeft = TwoFATries;
  237. }
  238. }
  239. private static Dictionary<Guid, AuthenticationCode> authenticationCodes = new();
  240. private static readonly int TwoFATries = 3;
  241. public static readonly int CodeLength = 6;
  242. private static readonly TimeSpan Expiry2FACodeTime = TimeSpan.FromMinutes(15);
  243. private static Dictionary<SMSProviderType, ISMSProvider> SMSProviders { get; set; } = new();
  244. public static void AddSMSProvider(ISMSProvider provider)
  245. {
  246. SMSProviders.Add(provider.ProviderType, provider);
  247. }
  248. private static string GenerateCode()
  249. {
  250. var random = new Random(DateTime.Now.Millisecond);
  251. var code = "";
  252. for (int i = 0; i < CodeLength; i++)
  253. {
  254. code += random.Next(10).ToString();
  255. }
  256. return code;
  257. }
  258. public static Guid? SendCode(Guid userGuid, out string? recipient)
  259. {
  260. EnsureCache(false);
  261. var user = _cache.FirstOrDefault(x => x.ID == userGuid);
  262. if(user == null)
  263. {
  264. Logger.Send(LogType.Error, "", "Cannot send code; user does not exist!");
  265. recipient = null;
  266. return null;
  267. }
  268. var session = NewSession(user, false);
  269. Logger.Send(LogType.Information, "", $"New login session {session} for {user.UserID}");
  270. if (user.TwoFactorAuthenticationType != TwoFactorAuthenticationType.GoogleAuthenticator)
  271. {
  272. var smsProvider = SMSProviders
  273. .Where(x => x.Value.TwoFactorAuthenticationType == user.TwoFactorAuthenticationType)
  274. .Select(x => x.Value).FirstOrDefault();
  275. if (smsProvider == null)
  276. {
  277. Logger.Send(LogType.Error, "", "Cannot send code; user requests a 2FA method which is not supported!");
  278. recipient = null;
  279. return null;
  280. }
  281. var code = GenerateCode();
  282. Logger.Send(LogType.Information, "", $"Code for session {userGuid} is {code}");
  283. authenticationCodes.Add(session, new AuthenticationCode(code, DateTime.Now + Expiry2FACodeTime));
  284. var recAddr = user.Recipient2FA;
  285. if (smsProvider.SendMessage(recAddr, $"Your authentication code is {code}. This code will expire in {Expiry2FACodeTime.Minutes} minutes."))
  286. {
  287. Logger.Send(LogType.Information, "", "Code sent!");
  288. var first = recAddr[..3];
  289. var last = recAddr[^3..];
  290. recipient = first + new string('*', recAddr.Length - 6) + last;
  291. return session;
  292. }
  293. else
  294. {
  295. Logger.Send(LogType.Information, "", "Code failed to send!");
  296. recipient = null;
  297. return null;
  298. }
  299. }
  300. else
  301. {
  302. Logger.Send(LogType.Information, "", $"Google authenticator is being used");
  303. recipient = "Google Authenticator";
  304. return session;
  305. }
  306. }
  307. private static readonly int CodeModulo = (int)Math.Pow(10, CodeLength);
  308. private static string GenerateGoogleAuthenticatorCode(long time, byte[] key)
  309. {
  310. var window = time / 30;
  311. var hex = window.ToString("x");
  312. if (hex.Length < 16)
  313. {
  314. hex = hex.PadLeft(16, '0');
  315. }
  316. var bytes = Convert.FromHexString(hex);
  317. var hash = new HMACSHA1(key).ComputeHash(bytes);
  318. var offset = hash[hash.Length - 1] & 0xf;
  319. var selected = new byte[4];
  320. Buffer.BlockCopy(hash, offset, selected, 0, 4);
  321. if (BitConverter.IsLittleEndian)
  322. {
  323. Array.Reverse(selected);
  324. }
  325. var integer = BitConverter.ToInt32(selected, 0);
  326. var truncated = integer & 0x7fffffff;
  327. return (truncated % CodeModulo).ToString().PadLeft(CodeLength, '0');
  328. }
  329. private static bool CheckAuthenticationCode(byte[] token, string code)
  330. {
  331. var time = DateTimeOffset.Now.ToUnixTimeSeconds();
  332. for (long i = time - 30; i <= time; i += 30)
  333. {
  334. if(GenerateGoogleAuthenticatorCode(i, token) == code)
  335. {
  336. return true;
  337. }
  338. }
  339. return false;
  340. }
  341. public static bool ValidateCode(Guid sessionID, string code)
  342. {
  343. if (!sessions.TryGetValue(sessionID, out var session))
  344. {
  345. return false;
  346. }
  347. bool valid;
  348. if(authenticationCodes.TryGetValue(sessionID, out var result))
  349. {
  350. if (result.Code != code)
  351. {
  352. result.TriesLeft--;
  353. if (result.TriesLeft == 0)
  354. {
  355. authenticationCodes.Remove(sessionID);
  356. }
  357. valid = false;
  358. }
  359. else if (result.Expiry < DateTime.Now)
  360. {
  361. authenticationCodes.Remove(sessionID);
  362. valid = false;
  363. }
  364. else
  365. {
  366. valid = true;
  367. }
  368. }
  369. else
  370. {
  371. var user = _cache.FirstOrDefault(x => x.ID == session.User);
  372. if(user?.TwoFactorAuthenticationType == TwoFactorAuthenticationType.GoogleAuthenticator)
  373. {
  374. valid = CheckAuthenticationCode(user.AuthenticatorToken, code);
  375. }
  376. else
  377. {
  378. valid = false;
  379. }
  380. }
  381. if (valid)
  382. {
  383. session.Valid = true;
  384. return true;
  385. }
  386. else
  387. {
  388. return false;
  389. }
  390. }
  391. #endregion
  392. }
  393. }