/// <summary> /// Checks whether the provided password is in the Enzoic database of known, compromised passwords. /// @see <a href="https://www.enzoic.com/docs/passwords-api">https://www.enzoic.com/docs/passwords-api</a> /// </summary> /// <param name="password">The password to be checked</param> /// <param name="revealedInExposure">Out parameter. Whether the password was exposed in a known data Exposure. If this value /// is false, the password was found in common password cracking dictionaries, but has not been directly exposed as a user /// password in a data breach or other Exposure.</param> /// <param name="relativeExposureFrequency">This is a gauge of how frequently the password has been seen in data breaches. /// The value is simply the percent of data /// breaches indexed by Enzoic that have contained at least one instance of this password, i.e. if the value is 13, /// that means 13% of the exposures that Enzoic has indexed contained this password at least one time. This value can /// be used to gauge how dangerous this password is by how common it is.</param> /// <returns>True if the password is a known, compromised password and should not be used</returns> public bool CheckPassword(string password, out bool revealedInExposure, out int?relativeExposureFrequency) { string md5 = Hashing.CalcMD5(password); string sha1 = Hashing.CalcSHA1(password); string sha256 = Hashing.CalcSHA256(password); String response = MakeRestCall( apiBaseURL + PASSWORDS_API_PATH + "?partial_md5=" + md5.Substring(0, 10) + "&partial_sha1=" + sha1.Substring(0, 10) + "&partial_sha256=" + sha256.Substring(0, 10), "GET", null); if (response != "404") { dynamic responseObj = JObject.Parse(response); foreach (dynamic candidate in responseObj.candidates) { if (candidate.md5 == md5 || candidate.sha1 == sha1 || candidate.sha256 == sha256) { revealedInExposure = candidate.revealedInExposure; relativeExposureFrequency = candidate.relativeExposureFrequency; return(true); } } } revealedInExposure = false; relativeExposureFrequency = null; return(false); }
/// <summary> /// Calls the Enzoic CheckCredentials API in a secure fashion to check whether the provided username and password /// are known to be compromised. /// This call is made securely to the server - only a salted and hashed representation of the credentials are passed and /// the salt value is not passed along with it. /// @see <a href="https://www.enzoic.com/docs/credentials-api">https://www.enzoic.com/docs/credentials-api</a> /// </summary> /// <param name="username">the username to check - may be an email address or username</param> /// <param name="password">the password to check</param> /// <param name="lastCheckDate">(Optional) The timestamp for the last check you performed for this user. If the date/time you provide /// for the last check is greater than the timestamp Enzoic has for the last breach affecting this user, the check will /// not be performed.This can be used to substantially increase performance.Can be set to null if no last check was performed /// or the credentials have changed since.</param> /// <param name="excludeHashTypes">(Optional) An array of PasswordTypes to ignore when calculating hashes for the credentials check. /// By excluding computationally expensive PasswordTypes, such as BCrypt, it is possible to balance the performance of this /// call against security.Can be set to null if you don't wish to exclude any hash types.</param> /// <returns>true if the credentials are known to be compromised, false otherwise</returns> public bool CheckCredentials(string username, string password, DateTime?lastCheckDate = null, PasswordType[] excludeHashTypes = null) { String response = MakeRestCall( apiBaseURL + ACCOUNTS_API_PATH + "?username="******"GET", null); if (response == "404") { // this is all we needed to check for this - email wasn't even in the DB return(false); } // deserialize response AccountsResponse accountsResponse = JsonConvert.DeserializeObject <AccountsResponse>(response); // see if the lastCheckDate was later than the lastBreachDate - if so bail out if (lastCheckDate.HasValue && lastCheckDate.Value >= accountsResponse.lastBreachDate) { return(false); } int bcryptCount = 0; List <string> credentialHashes = new List <string>(); StringBuilder queryString = new StringBuilder(); foreach (PasswordHashSpecification hashSpec in accountsResponse.PasswordHashesRequired) { if (excludeHashTypes != null && excludeHashTypes.Contains(hashSpec.HashType)) { // this type is excluded continue; } // bcrypt gets far too expensive for good response time if there are many of them to calculate. // some mostly garbage accounts have accumulated a number of them in our DB and if we happen to hit one it // kills performance, so short circuit out after at most 2 BCrypt hashes if (hashSpec.HashType != PasswordType.BCrypt || bcryptCount <= 2) { if (hashSpec.HashType == PasswordType.BCrypt) { bcryptCount++; } String credentialHash = CalcCredentialHash(username, password, accountsResponse.Salt, hashSpec); if (credentialHash != null) { credentialHashes.Add(credentialHash); if (queryString.Length == 0) { queryString.Append("?partialHashes=").Append(credentialHash.Substring(0, 10)); } else { queryString.Append("&partialHashes=").Append(credentialHash.Substring(0, 10)); } } } } if (queryString.Length > 0) { String credsResponse = MakeRestCall( apiBaseURL + CREDENTIALS_API_PATH + queryString, "GET", null); if (credsResponse != "404") { // loop through candidate hashes returned and see if we have a match with the exact hash dynamic responseObj = JObject.Parse(credsResponse); foreach (dynamic candidate in responseObj.candidateHashes) { if (credentialHashes.FirstOrDefault(hash => hash == candidate.ToString()) != null) { return(true); } } } } return(false); }