Hashing client side
Secondly assuming that the connection is compromised because of an MiTM attack. The process of how the leaked hash of the password is created is still unknown because the salt and iterations (based on the pincode) are unknown.
In case of a MitM attack (made possible by say incorrect use of TLS) hashing client side will not help you. The attacker could just modify the JavaScript sent to the client so that it sends the password and PIN in clear text. Short of having the user read and verify the complete JavaScript code at every login there is no way to protect your password if an attacker breakes the TLS.
Using a PIN that is "never stored"
I think the combination of salting and stretching locally based on a pin-code that is never stored but just known by the user makes it harder to guess the salt and also to reproduce the exact process without the user since the local used salt and amount of stretching will be unknown to the server.
If I understand you correctly, your goal is to make it harder to make an offline brute force attack on the password hashes, by using a salt that is never stored.
Let me recap the main points of your scheme to establish some terminology. We have a hash function: HASH(data, salt, itteration count)
. The user has secrets P1
(password) and P2
(PIN). P2
is hashed locally with N
itterations and using P2
as salt: H1 = HASH(P1, P2, N)
. H2
is sent to the server, where it hashed again (with a user specific salt and M
itterations) : H2 = HASH(H1, salt, M)
.
So how would an attacker in posession of H2
brute force this? She would just use a normal dictionary attack, looping through possible combinations of P1
and P2
. First she would recreate the process on the client to get H1
and then she would recreate the process on the server to get H2
, and compare it with the actual value to see if she picket the right secrets.
This would take N + M
itterations per attempt, and she would have to make X ^ (LENGTH(P1) + LENGTH(P2))
(where X
is the size of the alphabet) attempts to try all possible secrets.
Now compare this to a scheme where there would just be one password with the combined length of P1
and P2
, that is only hashed on the server with N + M
itterations. Would your scheme be any better than this? No, it would be exactly the same. That would also take N + M
itterations per attempt, and X ^ (LENGTH(P1) + LENGTH(P2))
attempts in total.
Long story short, your scheme is no better than just using a longer password. The only difference is that you have split the password in two parts, complicated things a bit, and done some hashing on the client.
Or in more general terms: You can not increase security by doing something with the password client side. If you do F(P)
and send that instead of P
, the attacker will also do F(P)
when she brute force and you have gained nothing.
Some thoughts about rolling your own
You are rolling your own here - that is, you are coming up with your own solution to a problem where an established best practice already exists. This has been discussed here many times before:
Rolling your own system when there are established solutiongs seldom leads anywhere. You add more complexity, but you seldom add any security. If you are lucky, at least you don't make things worse. But if you are unlucky, there is no limit to the problems you can cause for yourself.
In your case, I don't think you did any fatal mistakes leading to obvious vulnerabilities. But who knows? Not me, not you. And when you implement this complex system, there are so many more places where you can make a tiny mistake and ruin it.
That said, speculating about alternative solutions can be a very useful learning experience and help us understand existing practices and concepts better. So there is nothing wrong in proposing and discussing different systems, as long as you don't roll them out into production.