private String CalcCredentialHash(String username, String password, String salt, PasswordHashSpecification specification) { String passwordHash = Hashing.CalcPasswordHash(specification.HashType, password, specification.Salt); if (passwordHash != null) { String argon2Hash = Hashing.CalcArgon2(username + "$" + passwordHash, salt); return(argon2Hash.Substring(argon2Hash.LastIndexOf('$') + 1)); } else { return(null); } }
/// <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> /// <param name="useRawCredentials">(Optional) Pass true in for this parameter to use the Raw Credentials /// variant of the Credentials API. The Raw Credentials version of the Credentials API allows you to /// check usernames and passwords for compromise without passing even a hashed version to Enzoic. /// This works be pulling down all of the Credentials Hashes Enzoic has for a given username and /// calculating/comparing locally. The only thing that gets passed to Enzoic in this case is a hash of /// the username. Raw Credentials requires a separate approval to unlock. If you're interested in getting /// approved, please contact us through our website.</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, bool useRawCredentials = false) { String response = MakeRestCall( BaseURL + ACCOUNTS_API_PATH + "?username="******"&includeHashes=1" : ""), "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); } if (accountsResponse.CredentialsHashes != null) { int bcryptCount = 0; foreach (CredentialsHashSpecification credHashSpec in accountsResponse.CredentialsHashes) { PasswordHashSpecification hashSpec = new PasswordHashSpecification { Salt = credHashSpec.Salt, HashType = credHashSpec.HashType }; 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) { if (credentialHash == credHashSpec.CredentialsHash) { return(true); } } } } } else if (accountsResponse.PasswordHashesRequired != null) { 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( BaseURL + 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); }