One method that could be used would be to generate "acceptable typos" upon initial password entry, then to store the hashes of all of these. There are some problems with this: you increase the time required for a password change and for password verification, especially if you're using a slow password hashing method, as usually recommended, and you increase the storage requirements by a potentially variable amount.
Assume that the password used is "Password1!", and that "acceptable typos" are the case of individual letters (or the presence of appropriate characters from pressing Shift and a number), the swapping of adjacent characters, and missing a single instance of a duplicated character (these are not the only possible typos, but from my typos, they're pretty easy to do!).
That means that all of the following would be acceptable:
Password1! [transposing "s" and "s"]
Given any reasonable password algorithm (or MD5), these will all have unrelated hashes:
To verify a password in this case, you hash the input, and check all the possible values. You can even salt the input on an account level, and you only have to calculate one hash. There is a flaw though.
Knowing all of these hashes doesn't actually help an attacker directly, but they can apply the same rules as you did to any password list they have, and should any of the hashes match, they get access to the account - this applies if you use an account specific salt too. The trade-off for login speed helps them too.
Luckily, it's quite easy to defend against that, with a hash specific salt. If you use most implementations of bcrypt, you get this for free:
Now they have to try all of the modifications against each stored hash. Depending on how you store your passwords, they might be able to optimise which variations they try against each hash, but since they don't know whether the correct password is "Password1!" or "pAssword1!", there is a risk of missing things if they do. However, when someone wants to log in, you need to do the same.
This is the trade-off - if you use a hashing method which takes 100ms to calculate, and need to calculate 1 value, it'll take 100ms to check. If you need to check 10 versions, it'll take 1s of processing time. If you do the checks one by one, you get a timing issue - fast responses mean the original password has been found, slower ones mean it has a typo. If you do them in parallel, you've increased the computing requirements for your system.
Using just the above list of "acceptable typos", you get a variable number of potential passwords. For "Password1!", that means you're storing 21 hashes, one of which happens to be a duplicate of the original password. If you require constant time output when logging into your system (this is a good thing), you either need to be able to calculate all these hashes in a given time, or to delay every login to the time it would take to calculate all the hashes for the longest password you support. If you don't, you've made a password length oracle.
There are hashing methods which allow for similar input to result in similar hashes, but these don't tend to have desirable properties for hashing passwords. They are generally optimised for finding similar parts of large data structures, so are high speed, meaning that large numbers of potential passwords can be tried quickly. They, by definition, don't have an avalanche effect, meaning that a potential attack could try common passwords, then pick the "closest" hash to the ones found for additional mutations. If an output hash from a mutation is "closer" to the stored ones, the input is also closer. With a cryptographic hash, even a fast one, changing a single character in the input is expected to change most of the output. There isn't a concept of "closer" in terms of output, so an attacker can't do better than random changes, hoping to get a match.
If you avoid the hashing issue completely, and go down the HSM route, it might be feasible to have a HSM which stores plain text passwords in an unrecoverable way, and which could produce responses of "original", "typo" and "no" using classical comparison methods. This might be acceptable if you allowed some actions to logged in users, but then required the correct password to be provided for high risk operations (e.g. changing the password), but it's not likely to be cost effective for most providers.
It's certainly possible, but there are a lot of potential issues with doing it, and it's unclear whether the UX benefits would justify the effort. It would probably be a lot easier to encourage the use of password manager applications, since they'll avoid the risk of typos in a lot of cases!