So I am programming a timetable app in meteor, using the UNTIS backend. The problem I'm facing right now is, that I don't want each user to re-enter their passwords each time there is a request to the server. Sometimes I can't even (for example at 6 am to check if the first lesson isn't cancelled after all).
The problem is, that the password is needed in plain. So the password has to be accesible in plain at some point or another on the server.
I created a Meteor settings file:
/development.json
{
"ENCRYPT_PASSW_ENCRYPTION_KEY": "*some long encryption key*",
"ENCRYPT_PASSW_SALT_LENGTH": 32,
"ENCRYPT_PASSW_USER_KEY_LENGTH": 32,
"ENCRYPT_PBKDF2_ROUNDS": 100,
"ENCRYPT_PBKDF2_DIGEST": "sha512",
"ENCRYPT_PASSW_ALGORITHM": "aes-256-ctr"
}
In order to be able to use these settings in your meteor app, you will have to start meteor like this: meteor run --settings development.json
Sidenote: Of course you have to add your own parameters. These are just the development settings. You will have to choose your own parameters, depending on the importance of your data. (PBKDF2_ROUNDS should be chosen to fit your host system. I've read somewhere that a hash should take at least 241 milliseconds)
Some server side functions:
// server/lib/encryption.js
const crypto = require("crypto");
// generate a cryptograhpically secure salt
// with the length specified in the settings
generateUserSalt = function (length = Meteor.settings.ENCRYPT_PASSW_SALT_LENGTH) {
return crypto.randomBytes(length).toString("base64");
}
// encrypt a password with a key, derived from the
// application key plus the users salt
encryptUserPass = function (uid, pass, salt = false) {
const key = getUserKey(uid, salt),
algorithm = Meteor.settings.ENCRYPT_PASSW_ALGORITHM,
cipher = crypto.createCipher(algorithm, key);
return cipher.update(pass,'utf8','hex') + cipher.final('hex');
}
// decrypt a password with the same key
decryptUserPass = function (uid, ciphertext, salt = false) {
const key = getUserKey(uid, salt),
algorithm = Meteor.settings.ENCRYPT_PASSW_ALGORITHM,
decipher = crypto.createDecipher(algorithm, key);
return decipher.update(ciphertext,'hex','utf8') + decipher.final('utf8');
}
// generate the user-specific key that derives from
// the applications main encryption key plus the users
// specific salt. this is only needed in this scope
function getUserKey (uid, salt = false) {
// if no salt is given, take it from the user db
if (salt === false) {
const usr = Meteor.users.findOne(uid);
if (!usr || !usr.api_private || !usr.api_private.salt) {
throw new Meteor.Error("no-salt-given", "The salt from user with id" + uid + " couldn't be located. Maybe it's not set?");
}
salt = usr.untis_private.salt;
}
const systemKey = Meteor.settings.ENCRYPT_PASSW_ENCRYPTION_KEY,
rounds = Meteor.settings.ENCRYPT_PBKDF2_ROUNDS,
length = Meteor.settings.ENCRYPT_PASSW_USER_KEY_LENGTH,
digest = Meteor.settings.ENCRYPT_PBKDF2_DIGEST;
const userKey = crypto.pbkdf2Sync(systemKey, salt, rounds, length, digest);
return userKey.toString('hex');
}
And now I can encrypt each users password with a unique key like this:
// either with generating a salt (then the uid is not needed)
let salt = generateUserSalt(),
encPass = encryptUserPass(0, pass, salt);
// or when the user already has a salt (salt is in db)
let encPass2 = encryptUserPass(uid, pass);
Decrypting is also verry easy:
let pass = decryptUserPass(0, passEnc, salt);
// or
let pass2 = decryptUserPass(uid, passEnc);
Of course I know, that this is still pretty bad in terms of security (storing things on the server that can be reversed into user's passwords). The reason why I think this is okay:
Each users password is encrypted like this:
AES(password, PBKDF2(global-encryption-key + salt))
This means that:
Why I think this is a good sollution:
Therefore you should chose a rather large global-encryption-key.