Search code examples
securitymeteorpassword-protectionpassword-encryptionmeteor-accounts

Storing user-specific API keys in DB


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.


Solution

  • The way I solved this problem:

    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);
    

    Explanation

    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:

    1. each user's password is encrypted with a different key
    2. no encryption keys are saved in the db

    Why I think this is a good sollution:

    1. In case the database is leaked, the attacker will firstly need to guess the AES key correctly for one specific user and then reverse the PBKDF2 to find the global-encryption-key. or
    2. Guess the global-encryption-key

    Therefore you should chose a rather large global-encryption-key.

    Facts about salts

    1. NEVER use a salt twice
    2. Change salts together with passwords (don't reuse them)
    3. Salts should be long: rule of thumb: make the salt as long the output of the hash function (sha256 = 32 bytes)

    more about salting things