/* * Tries to login with the given cookie as credentials */ static void LoginFromCookie(HttpCookie cookie, ApplicationContext context) { // User has persistent cookie associated with client var cookieSplits = cookie.Value.Split(' '); if (cookieSplits.Length != 2) { throw new SecurityException("Cookie not accepted"); } string cookieUsername = cookieSplits [0]; string hashedPassword = cookieSplits [1]; Node pwdFile = AuthFile.GetAuthFile(context); // Checking if user exist Node userNode = pwdFile ["users"] [cookieUsername]; if (userNode == null) { throw new SecurityException("Cookie not accepted"); } // Notice, we do NOT THROW if passwords do not match, since it might simply mean that user has explicitly created a new "salt" // to throw out other clients that are currently persistently logged into system under his account if (hashedPassword == userNode ["password"].Get <string> (context)) { // MATCH, discarding previous Context Ticket and creating a new Ticket SetTicket(new ContextTicket( userNode.Name, userNode ["role"].Get <string> (context), false)); LastLoginAttemptForIP = DateTime.MinValue; } }
/* * Returns all existing roles in system */ public static void GetRoles(ApplicationContext context, Node args) { // Making sure default role is added first. string defaultRole = context.Raise(".p5.auth.get-default-context-role").Get <string> (context); if (!string.IsNullOrEmpty(defaultRole)) { // There exist a default role, checking if it's already added if (args.Children.FirstOrDefault(ix => ix.Name == defaultRole) == null) { // Default Role was not already added, therefor we add it to return lambda node args.Add(defaultRole); } } // Getting password file in Node format, such that we can traverse file for all roles Node pwdFile = AuthFile.GetAuthFile(context); // Looping through each user object in password file, retrieving all roles foreach (var idxUserNode in pwdFile["users"].Children) { // Retrieving role name of currently iterated user var role = idxUserNode["role"].Get <string>(context); // Adding currently iterated role, unless already added, and incrementing user count for it args.FindOrInsert(role).Value = args[role].Get <int> (context, 0) + 1; } }
/* * Returns server-salt for application */ public static string ServerSalt(ApplicationContext context) { // Retrieving "auth" file in node format var authFile = AuthFile.GetAuthFile(context); return(authFile.GetChildValue <string> ("server-salt", context)); }
/* * Returns true if root account's password is null, which means that server is not setup yet */ public static bool NoExistingRootAccount(ApplicationContext context) { // Retrieving password file, and making sure we lock access to file as we do Node rootPwdNode = AuthFile.GetAuthFile(context)["users"]["root"]; // Returning true if root account does not exist return(rootPwdNode == null); }
/* * Tries to login with the given cookie as credentials */ static void LoginFromCookie(HttpCookie cookie, ApplicationContext context) { // User has persistent cookie associated with client var cookieSplits = cookie.Value.Split(' '); if (cookieSplits.Length != 2) { throw new SecurityException("Cookie not accepted"); } // Retrieving username and (hashed/salted) password from cookie. var cookieUsername = cookieSplits [0]; var cookiePassword = cookieSplits [1]; // Retrieving password file in Node format. var pwdFile = AuthFile.GetAuthFile(context); // Checking if user exist. var userNode = pwdFile ["users"] [cookieUsername]; if (userNode == null) { throw new SecurityException("Cookie not accepted"); } /* * Checking if user's password is a match. * * Notice, we need to double hash the password from "auth", since the * reference stored in cookie is double hashed, and has the client's fingerprint * added to it, to reduce probability of cookie theft. * * Notice also that since the credential cookie has roughly 1.0e+77 amount of * entropy, slow hashing or using blow fish at this point is pointless, and * would simply add additional overhead for the initial loading of our page * when the persistent credential cookie needs to be verified. Hence we * can safely get away with "fast hashing" at this point, but adding parts * of the clients fingerprint into the mix, to avoid at least to some extent * credential cookie theft. */ if (cookiePassword == Passwords.HashPasswordForCookieStorage( context, userNode ["password"].Get <string> (context))) { // MATCH, discarding previous Context Ticket and creating a new Ticket. SetTicket(context, new ContextTicket(userNode.Name, userNode ["role"].Get <string> (context), false)); // Evaluates user's [.onlogin] section, if it exists. EvaluateOnLoginIfExisting(context); } else { // Catched above, which destroys cookie, and associates the default context with user. throw new Exception(); } }
/* * Lists all users in system */ public static void ListUsers(ApplicationContext context, Node args) { // Retrieving "auth" file in node format var authFile = AuthFile.GetAuthFile(context); // Looping through each user in [users] node of "auth" file foreach (var idxUserNode in authFile["users"].Children) { // Returning user's name, and role he belongs to args.Add(idxUserNode.Name, idxUserNode["role"].Value); } }
/* * Returns all access objects for system. */ public static void ListAccess(ApplicationContext context, Node args) { // Getting password file in Node format, such that we can traverse file for all roles Node pwdFile = AuthFile.GetAuthFile(context); // Checking if we have any custom access rights in system. if (pwdFile ["access"] == null) { return; } // Checking which role caller requests access objects on behalf. string roles = null; if (context.Ticket.Role == "root") { // Checking if caller requested a particular role. roles = args.GetExChildValue <string> ("role", context, null); } else { // A non-root user is not allowed to request anything besides his own access objects. if (args ["role"] != null) { throw new LambdaException("A non-root user cannot request access objects for anything but his own role", args, context); } roles = context.Ticket.Role; } // Looping through each user object in password file, retrieving all roles foreach (var idxUserNode in pwdFile["access"].Children) { // Adding currently iterated access to return args. if (roles == null) { // Adding everything. args.Add(idxUserNode.Clone()); } else { // Adding only access objects requested by caller. if (idxUserNode.Name == "*" || idxUserNode.Name == roles) { args.Add(idxUserNode.Clone()); } } } }
/* * Retrieves settings for currently logged in user */ public static void GetSettings(ApplicationContext context, Node args) { // Retrieving "auth" file in node format var authFile = AuthFile.GetAuthFile(context); // Checking if user exist if (authFile ["users"] [context.Ticket.Username] == null) { throw new LambdaException( "You do not exist", args, context); } args.AddRange(authFile["users"][context.Ticket.Username].Clone().Children.Where(ix => ix.Name != "password" && ix.Name != "role")); }
/* * Returns server-salt for application */ public static string ServerSalt(ApplicationContext context) { // Retrieving "auth" file in node format. var authFile = AuthFile.GetAuthFile(context); var authSalt = authFile.GetChildValue <string> ("server-salt", context); // Notice, if "auth file" salt is not (yet) set, we return null to caller, to // signal that no salt has been initialized yet. if (string.IsNullOrEmpty(authSalt)) { return(null); } var configSalt = context.RaiseEvent(".p5.config.get", new Node(".p5.config.get", ".p5.crypto.salt")).Get(context, "$41t-goes-here-4U"); return(authSalt + configSalt); }
/* * Lists all users in system */ public static void ListUsers(ApplicationContext context, Node args) { // Retrieving "auth" file in node format var authFile = AuthFile.GetAuthFile(context); // Retrieving guest account name, to make sure we exclude it as a user, since it's not a "real user" per se. var guestAccountName = context.RaiseEvent(".p5.auth.get-default-context-username").Get <string> (context); // Looping through each user in [users] node of "auth" file foreach (var idxUserNode in authFile["users"].Children) { if (idxUserNode.Name == guestAccountName) { continue; } // Returning user's name, and role he belongs to args.Add(idxUserNode.Name, idxUserNode ["role"].Value); } }
/* * Retrieves settings for currently logged in user. */ public static void GetSettings(ApplicationContext context, Node args) { // Retrieving "auth" file in node format. var authFile = AuthFile.GetAuthFile(context); // Checking if user exist if (authFile ["users"] [context.Ticket.Username] == null) { throw new LambdaException( "You do not exist", args, context); } // Retrieving user node. var userNode = authFile ["users"] [context.Ticket.Username]; // Checking if caller is retieving a single section. var section = args.GetExValue(context, ""); if (string.IsNullOrEmpty(section)) { // All settings invocation. args.AddRange(userNode.Children.Where(ix => ix.Name != "password" && ix.Name != "role").Select(ix => ix.Clone())); } else if (section != "password" && section != "role") { // Single section invocation. var sectionNode = userNode [section]?.Clone(); if (sectionNode != null) { args.Add(sectionNode); } } else { // Illegal attempt at trying to retrieve role or password. throw new LambdaSecurityException("Illegal invocation, you can't retrieve [password] or [role]", args, context); } }
/* * Tries to login with the given cookie as credentials */ private static void LoginFromCookie(HttpCookie cookie, ApplicationContext context) { // User has persistent cookie associated with client var cookieSplits = cookie.Value.Split(' '); if (cookieSplits.Length != 2) { throw new SecurityException("Cookie not accepted"); } string cookieUsername = cookieSplits[0]; string cookieHashSaltedPwd = cookieSplits[1]; Node pwdFile = AuthFile.GetAuthFile(context); // Checking if user exist Node userNode = pwdFile["users"][cookieUsername]; if (userNode == null) { throw new SecurityException("Cookie not accepted"); } // Getting system salt var serverSalt = context.Raise(".p5.auth.get-server-salt").Get <string> (context); // Then creating system fingerprint from given password var systemFingerprint = context.Raise("p5.crypto.hash.create-sha256", new Node("", serverSalt + cookieHashSaltedPwd)).Get <string> (context); // Notice, we do NOT THROW if passwords do not match, since it might simply mean that user has explicitly created a new "salt" // to throw out other clients that are currently persistently logged into system under his account if (systemFingerprint == userNode["password"].Get <string> (context)) { // MATCH, discarding previous Context Ticket and creating a new Ticket SetTicket(new ApplicationContext.ContextTicket( userNode.Name, userNode ["role"].Get <string>(context), false)); LastLoginAttemptForIP = DateTime.MinValue; } }
/* * Retrieves a specific user from system */ public static void GetUser(ApplicationContext context, Node args) { // Retrieving "auth" file in node format var authFile = AuthFile.GetAuthFile(context); // Iterating all users requested by caller foreach (var idxUsername in XUtil.Iterate <string> (context, args)) { // Checking if user exist if (authFile ["users"] [idxUsername] == null) { throw new LambdaException( string.Format("User '{0}' does not exist", idxUsername), args, context); } // Adding user's node as return value, and each property of user, except [password] args.Add(idxUsername); args [idxUsername].AddRange(authFile["users"][idxUsername].Clone().Children.Where(ix => ix.Name != "password")); } }
/* * Tries to login user according to given user credentials. */ public static void Login(ApplicationContext context, Node args) { // Defaulting result of Active Event to unsuccessful. args.Value = false; // Retrieving supplied credentials. var username = args.GetExChildValue <string> ("username", context); var password = args.GetExChildValue <string> ("password", context); args.FindOrInsert("password").Value = "xxx"; // In case an exception occurs. var persist = args.GetExChildValue("persist", context, false); // Retrieving password file as a Node. var pwdFile = AuthFile.GetAuthFile(context); /* * Checking for match on specified username. */ var userNode = pwdFile ["users"] [username]; if (userNode == null) { // Username doesn't exist. throw new LambdaSecurityException(_credentialsNotAcceptedException, args, context); } /* * Checking if current username has attempted to login just recently, and the * configured timespan for each successive login attempt per user, has not passed. * * This should be able to provide some rudimentary defense from a "brute force password attack". * * Notice, we do this after we have checked if the username exists, to avoid having an adversary * flood the server's cache with bogus usernames associated with DateTime objects. * We also us the web cache, which (of course) means the object will only exists for some * time, defaulting to the settings of the web cache for your system. */ var cooldown = context.RaiseEvent( ".p5.config.get", new Node(".p5.config.get", _cooldownPeriodConfigName)) [0]?.Get(context, -1) ?? -1; if (cooldown != -1) { // User has configured the system to have a "cooldown period" for successive login attempts. var bruteForceLastAttempt = new Node(".p5.web.cache.get", _bruteForceCacheName + username); var lastAttemptNode = context.RaiseEvent(".p5.web.cache.get", bruteForceLastAttempt); if (lastAttemptNode.Count > 0) { // Previous attempt has been attempted recently. var date = lastAttemptNode [0].Get <DateTime> (context, DateTime.MinValue); var timeSpanSeconds = Convert.ToInt32((DateTime.Now - date).TotalSeconds); if (timeSpanSeconds < cooldown) { // Cooldown period has not passed. throw new LambdaException("You need to wait " + (cooldown - timeSpanSeconds) + " seconds before you can try again", args, context); } } } // Checking for match on password. if (!Passwords.VerifyPasswordIsCorrect(password, userNode ["password"].Get <string> (context))) { /* * Making sure we guard against brute force password attacks, before we throw security exception. * * Notice, this prevents the same username from attempting to login more than once every n seconds, * which is configurable in the config file of the app. */ var bruteForceLastAttempt = new Node(".p5.web.cache.set", _bruteForceCacheName + username); bruteForceLastAttempt.Add("src", DateTime.Now); context.RaiseEvent(".p5.web.cache.set", bruteForceLastAttempt); throw new LambdaSecurityException(_credentialsNotAcceptedException, args, context); } // Success, creating our context ticket. var role = userNode ["role"].Get <string> (context); SetTicket(context, new ContextTicket(username, role, false)); // Signaling success to caller. args.Value = true; // Checking if we should create persistent cookie on disc to remember username/password for given client. if (persist) { // Caller wants to create persistent cookie to remember username/password. var cookie = new HttpCookie(_credentialCookieName); cookie.Expires = DateTime.Now.AddDays(context.RaiseEvent( ".p5.config.get", new Node(".p5.config.get", "p5.auth.credential-cookie-valid")) [0].Get <int> (context)); // To avoid JavaScript access to credential cookie. cookie.HttpOnly = true; /* * The value of our cookie is in "username hashed-password" format. * * This is an entropy of roughly 1.1579e+77, making a brute force attack * impossible, at least without a Rainbow/Dictionary attack, which should * be effectively prevented, by having a single static server salt, * which again is cryptographically secured and persisted to disc * in the "auth" file, and hence normally inaccessible for an adversary. * * Notice, we double hash the password we store in our cookie, to make * sure we never expose the parts of our password we store in our "auth" file. */ cookie.Value = username + " " + Passwords.HashPasswordForCookieStorage( context, userNode ["password"].Get <string> (context)); HttpContext.Current.Response.Cookies.Add(cookie); } // Evaluates user's [.onlogin] section, if it exists. EvaluateOnLoginIfExisting(context); }
/* * Returns all access objects for system. */ public static void ListAccess(ApplicationContext context, Node args) { // Getting password file in Node format. Node pwdFile = AuthFile.GetAuthFile(context); // Checking if caller wants to remove GUID IDs of access objects in the system. var keepGuids = args.GetExChildValue("guids", context, false); // Checking if we have any access rights in system. if (pwdFile ["access"] == null) { return; } // Checking which role caller requests access objects on behalf. string roles = null; if (context.Ticket.Role == "root") { // Checking if caller requested a particular role. roles = args.GetExChildValue <string> ("role", context, null); } else { // A non-root user is not allowed to request anything besides his own access objects. if (args ["role"] != null) { throw new LambdaException("A non-root user cannot request access objects for anything but his own role", args, context); } // Making sure user only retrieves access objects that are relevant for his own role. roles = context.Ticket.Role; } // Looping through each user object in password file, retrieving all roles. foreach (var idxAccessNode in pwdFile["access"].Children) { // Checking what types of access object(s) are relevant to caller. if (roles == null) { // Cloning access object node. var cur = idxAccessNode.Clone(); // Removing GUID IDs if caller specified we should do so. Guid guid; if (!keepGuids && Guid.TryParse(idxAccessNode.Get <string> (context), out guid)) { cur.Value = null; } // Caller wants everything. args.Add(cur); } else { // Checking if currently iterated access object is relevant to caller. if (idxAccessNode.Name == "*" || idxAccessNode.Name == roles) { // Cloning access object node. var cur = idxAccessNode.Clone(); // Removing GUID IDs if caller specified we should do so. Guid guid; if (!keepGuids && Guid.TryParse(idxAccessNode.Get <string> (context), out guid)) { cur.Value = null; } // Adding currently iterated access object. args.Add(cur); } } } }
/* * Tries to login user according to given user credentials */ public static void Login(ApplicationContext context, Node args) { // Defaulting result of Active Event to unsuccessful args.Value = false; // Retrieving supplied credentials string username = args.GetExChildValue <string> ("username", context); string password = args.GetExChildValue <string> ("password", context); bool persist = args.GetExChildValue("persist", context, false); // Getting password file in Node format, but locking file access as we retrieve it Node pwdFile = AuthFile.GetAuthFile(context); // Checking for match on specified username Node userNode = pwdFile["users"][username]; if (userNode == null) { throw new LambdaSecurityException("Credentials not accepted", args, context); } // Getting system salt var serverSalt = context.Raise(".p5.auth.get-server-salt").Get <string> (context); // Then creating system fingerprint from given password var cookiePasswordFingerprint = context.Raise("p5.crypto.hash.create-sha256", new Node("", serverSalt + password)).Get <string> (context); // Checking for match on password if (userNode["password"].Get <string> (context) != cookiePasswordFingerprint) { throw new LambdaSecurityException("Credentials not accepted", args, context); // Exact same wording as above! IMPORTANT!! } // Success, creating our ticket string role = userNode["role"].Get <string>(context); SetTicket(new ApplicationContext.ContextTicket(username, role, false)); args.Value = true; // Removing last login attempt, to reset brute force login cool off seconds for user's IP address LastLoginAttemptForIP = DateTime.MinValue; // Associating newly created Ticket with Application Context, since user now possibly have extended rights context.UpdateTicket(GetTicket(context)); // Checking if we should create persistent cookie on disc to remember username for given client if (persist) { // Caller wants to create persistent cookie to remember username/password HttpCookie cookie = new HttpCookie(_credentialCookieName); cookie.Expires = DateTime.Now.AddDays(context.Raise( ".p5.config.get", new Node(".p5.config.get", "p5.auth.credential-cookie-valid")) [0].Get <int> (context)); cookie.HttpOnly = true; // To avoid JavaScript access to credential cookie // Notice, we use another fingerprint as password for cookie than what we use for storing cookie in auth file // The "system salted fingerprint" hence never leaves the server // If this was not the case, then the system fingerprint would effectively BE the password, allowing anyone // who somehow gets access to "auth" file also to log in by creating false cookies cookie.Value = username + " " + cookiePasswordFingerprint; HttpContext.Current.Response.Cookies.Add(cookie); } // Making sure we invoke an [.onlogin] lambda callbacks for user. var onLogin = new Node(); GetSettings(context, onLogin); if (onLogin [".onlogin"] != null) { var lambda = onLogin[".onlogin"].Clone(); context.Raise("eval", lambda); } }
/* * Tries to login user according to given user credentials */ public static void Login(ApplicationContext context, Node args) { // Defaulting result of Active Event to unsuccessful. args.Value = false; // Retrieving supplied credentials string username = args.GetExChildValue <string> ("username", context); string password = args.GetExChildValue <string> ("password", context); args.FindOrInsert("password").Value = "xxx"; // In case an exception occurs. bool persist = args.GetExChildValue("persist", context, false); /* * Checking if current username has attempted to login just recently, and the * configured timespan for each successive login attempt per user, has not passed. * * This should be able to defend us from a "brute force password attack". */ var bruteConf = new Node(".p5.config.get", ".p5.auth.cooldown-period"); var cooldown = context.RaiseEvent(".p5.config.get", bruteConf) [0]?.Get(context, -1) ?? -1; if (cooldown != -1) { // User has configured the system to have a "cooldown period" for successive login attempts. var bruteForceLastAttempt = new Node(".p5.web.application.get", ".p5.io.last-login-attempt-for-" + username); var lastAttemptNode = context.RaiseEvent(".p5.web.application.get", bruteForceLastAttempt); if (lastAttemptNode.Count > 0) { // Previous attempt has been attempted. var date = lastAttemptNode [0].Get <DateTime> (context, DateTime.MinValue); int timeSpanSeconds = System.Convert.ToInt32((DateTime.Now - date).TotalSeconds); if (timeSpanSeconds < cooldown) { throw new LambdaException("You need to wait " + (cooldown - timeSpanSeconds) + " seconds before you can try again", args, context); } } } // Getting password file in Node format, but locking file access as we retrieve it Node pwdFile = AuthFile.GetAuthFile(context); // Checking for match on specified username Node userNode = pwdFile ["users"] [username]; if (userNode == null) { throw new LambdaSecurityException("Credentials not accepted", args, context); } // Getting system salt var serverSalt = context.RaiseEvent(".p5.auth.get-server-salt").Get <string> (context); // Then creating system fingerprint from given password var hashedPassword = context.RaiseEvent("p5.crypto.hash.create-sha256", new Node("", serverSalt + password)).Get <string> (context); // Checking for match on password if (userNode ["password"].Get <string> (context) != hashedPassword) { // Making sure we guard against brute force password attacks. var bruteForceLastAttempt = new Node(".p5.web.application.set", ".p5.io.last-login-attempt-for-" + username); bruteForceLastAttempt.Add("src", DateTime.Now); context.RaiseEvent(".p5.web.application.set", bruteForceLastAttempt); throw new LambdaSecurityException("Credentials not accepted", args, context); } // Success, creating our ticket string role = userNode ["role"].Get <string> (context); SetTicket(new ContextTicket(username, role, false)); args.Value = true; // Removing last login attempt, to reset brute force login cool off seconds for user's IP address LastLoginAttemptForIP = DateTime.MinValue; // Associating newly created Ticket with Application Context, since user now possibly have extended rights context.UpdateTicket(GetTicket(context)); // Checking if we should create persistent cookie on disc to remember username for given client if (persist) { // Caller wants to create persistent cookie to remember username/password var cookie = new HttpCookie(_credentialCookieName); cookie.Expires = DateTime.Now.AddDays(context.RaiseEvent( ".p5.config.get", new Node(".p5.config.get", "p5.auth.credential-cookie-valid")) [0].Get <int> (context)); cookie.HttpOnly = true; // To avoid JavaScript access to credential cookie // Notice, we use another fingerprint as password for cookie than what we use for storing cookie in auth file // The "system salted fingerprint" hence never leaves the server // If this was not the case, then the system fingerprint would effectively BE the password, allowing anyone // who somehow gets access to "auth" file also to log in by creating false cookies cookie.Value = username + " " + hashedPassword; HttpContext.Current.Response.Cookies.Add(cookie); } // Making sure we invoke an [.onlogin] lambda callbacks for user. var onLogin = new Node(); GetSettings(context, onLogin); if (onLogin [".onlogin"] != null) { var lambda = onLogin [".onlogin"].Clone(); context.RaiseEvent("eval", lambda); } }
/* * Returns true if root account exists, which implies that server has been setup. */ public static bool HasRootAccount(ApplicationContext context) { // Retrieving password file, and returning true if root user exists. return(AuthFile.GetAuthFile(context) ["users"] ["root"] != null); }
/* * Returns PGP key's fingerprint. */ public static string GetFingerprint(ApplicationContext context) { // Retrieving "auth" file in node format, for then to return fingerprint back to caller. return(AuthFile.GetAuthFile(context).GetChildValue <string> (GnuPgpFingerprintNodeName, context)); }