Many web applications have login functions based on user names and passwords, but the vast majority of login is not secure at all, it is no exaggeration to say that most programmers do not know how to ensure the security of user names and passwords.
Standard of safety
To want a landing system security, at least to ensure the following aspects.
Security of the original password
Many people for the user’s original password security, still stay in the level of illegal third party access, but in fact, the biggest threat to the original password, often comes from the system developers and server management personnel. These people can be intentionally collected or inadvertently leaked, and are often responsible for the disclosure of users’ original passwords. When building a login system, it should be fundamentally avoided that only the user and the keylogger know the original password.
So how do you do that? First of all, the password must be encrypted on the client, which can make the password obtained by the back end is already encrypted. On the one hand, the server cannot access the original password, and on the other hand, even if the communication is monitored, the third party cannot get the user’s original password even if it has obtained the encrypted ciphertext on the client that can be used for login.
Hash: Irreversible encryption
Password encryption is different from ordinary encryption, one is that the content is important, the second is that the password verification does not need the original text, to check whether a password is correct, just need to look at the result of its encryption and the correct password encryption result is consistent. After determining these two points, the encryption method only requires that the same string will get the same ciphertext after encryption. Hashes fulfill this requirement perfectly.
In the hash algorithm, the preferred is the SHA2 series, although security has been questioned because of SHA1, but at least so far there has been no proven flaw. MD5 is not recommended because it is used too much and the rainbow table is too extensive.
Another question, is it enough to hash once? Of course not, not only multiple hashes, but also mixed with data such as user names. For example, the original password can be encrypted on the client side using the following method:
sha256(
sha265(sha265(password)) + sha265(username)
)
Copy the code
In this way, the difficulty of the ciphertext reverse tweet is increased, and the user name is added to make the ciphertext of different users completely different even if the password is the same.
Encryption on the client side, basically, can only go so far, because one of the main problems is that the encryption algorithm on the client side is public.
Salt: Mix in random data
Although the password is encrypted on the client side, both the algorithm and the mixed user name are public. The rest of the encryption needs to be left to the back end.
Since the result of hashing the same string is constant, knowing the algorithm and ciphertext, it is theoretically possible to deduce the password backwards, the difficulty of which depends on the complexity of the user’s original password. So how do you make it exponentially harder to reverse? The answer is to add a random string to the original ciphertext to make the user’s password more complicated. This random string is salt.
The back end gets the password from the client and encrypts it again by adding salt hash. For example:
sha256(
sha256(username + sha256(password + salt)) + salt + sha256(username + salt)
)
Copy the code
Note that the storage of salt is critical and must be kept separate from user information.
Ciphertext and salt update with non-traceability
Now that the password has been hashed multiple times on the client and back end, with salt added, it seems secure enough. But we could be safer. That is to change the salt frequently so that the ciphertext field values in the user information table also change frequently. This will not work unless both the user information and the salt are retrieved.
So when do you change salt and ciphertext? Since the back end does not store the ciphertext of the client hash, salt and ciphertext can only be modified at login time.
Can the username itself be encrypted?
This may seem like a far-fetched idea, but in fact, a user name can be encrypted just like a password if it serves as a simple login credential. Because no matter register, log in or retrieve password, do not need the original user name. Note, however, that the username can only be hashed, not salted, otherwise there is no basis for finding salt.
The hash of user name can be divided into two parts, one is the client side hash, to the server side, can be hashed again.
In this Demo, user names will not be hashed.
Communication security
At the application level, it’s basically safe. Next comes client and communication security. The client environment is basically uncontrollable, so we can only think of ways to secure communication. However, you don’t have to think too hard, just use HTTPS.
In the process
Above summarized how to ensure the security of a user name password login system, here to see a login system to meet the requirements of the login process. The registration process is relatively simple, so I won’t go into details.
Demo is a simple Web login system with user names and passwords, and the code samples are taken from it.
Browser login
The browser does the following:
- Get the user name and password entered by the user
- Hash the user name and password to obtain the ciphertext on the browser
- Submit the user name and ciphertext to the back end
The main code is as follows, taken from client/app.js:
function encryptPwd(username, password) { username = username.toLowerCase(); return sha256( username + sha256 ( sha256(sha256(sha256(password))) + sha256(username) ) ); } $scope.login = function(){ if($scope.check()) { return; } $scope.successMessage = ''; $scope.errorMessage = ''; $scope.status = 'loading'; $resource('/user/login') .save({ username: $scope.username, password: encryptPwd($scope.username, $scope.password) }, function(res){ $scope.status = 'done'; $scope.successMessage = 'login successful! '; }, function(reason){ $scope.status = 'done'; $scope.errorMessage = reason.data || 'failed'; }); };Copy the code
Back-end password authentication
The back-end verification process is as follows:
- Obtain the user name submitted from the front-end and the ciphertext from the browser
- Query the salt ID in the database based on the user name
- Obtain the salt based on the salt ID, and calculate the back-end ciphertext based on the user name, browser ciphertext, and salt
- Query the user table based on the user name and the back-end ciphertext. If the result is displayed, the login information is correct and the browser replies that the login is successful
- Generate the new salt, compute the new back-end ciphertext, and update both to the database
Implementation of the code is as follows, taken from the app/controllers/user. The server. The controller. Js:
function encryptPwd(usr, pwd, salt){ usr = usr.toLowerCase(); return sha256( sha256(usr + sha256(pwd + salt)) + salt + sha256(usr + salt) ) } function login(req, res, next){ req.models.user .findOne({select:['username', 'saltId'], where: {username: username}}) .exec(function(err, userDoc){ if(err) return next(err); if(! userDoc) return next(new Error('username not exists')); req.models.salt .findOne({id: userDoc.saltId}) .exec(function(err, saltDoc){ if(err) return next(err); if(! saltDoc) return next(new Error('can NOT find salt')); var pwdHash = encryptPwd(username, password, saltDoc.salt); req.models.user .findOne({select: ['id'], where: {username: username, password: pwdHash }}) .exec(function(err, doc){ if(err) return next(err); if(! doc) return next(new Error('password error')); res.json({ username: username }); return updateSalt(saltDoc, userDoc, password, next); }); }); }); }Copy the code
Salt and ciphertext update
After the successful login response is returned to the user, the method to update the salt and ciphertext is called. The detailed process of this method is as follows:
- Generate and store new salt
- Generates a new back-end ciphertext based on the new salt, user name, and browser ciphertext
- Store the back-end ciphertext to the user information table
Implementation as follows, taken from the app/controllers/user. The server controller. Js:
function updateSalt(saltDoc, userDoc, passwordInputed, next){
saltDoc.salt = Math.random().toString(15).substr(3, 27);
saltDoc.save(function(err){
if(err) return next(err);
userDoc.password = encryptPwd(userDoc.username, passwordInputed, saltDoc.salt);
userDoc.save(function(err){
if(err) return next(err);
return next();
});
});
}
Copy the code
Demo
Demo is hosted on Github. The front-end uses AngularJS + Bootstrap and the back-end uses Node.js + Express + MongoDB, which is a typical MEAN application.
Waterline, the ORM middleware, is used for data storage (I have written two introduction articles before: Waterline, the Node.js ORM data manipulation middleware, and Waterline for Express projects). The main purpose of using it is to store user information and salt in different places. In this example, salt is added to a file with losa-disk, and user information is added to MongoDB with losa-mongo.
git clone https://github.com/stiekel/safe-username-password-login.git
cd safe-username-password-login
npm i
npm i -g gulp
gulp
Copy the code
Then open http://localhost:7102/ in your browser.