在DB中存储用户特定的API密钥

时间:2016-09-22 19:47:03

标签: security meteor password-protection password-encryption meteor-accounts

所以我正在使用UNTIS后端在meteor中编写时间表应用程序。我现在面临的问题是,每次向服务器发出请求时,我都不希望每个用户重新输入密码。有时我甚至不能(例如在早上6点检查第一课是否都没有取消)。

问题是,普通需要密码。因此,密码必须在服务器上的某个位置可以正常访问。

1 个答案:

答案 0 :(得分:1)

我解决这个问题的方法:

我创建了一个Meteor设置文件:

/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"
}

为了能够在流星应用程序中使用这些设置,您必须像这样开始流星:meteor run --settings development.json

旁注:当然你必须添加自己的参数。这些只是开发设置。您必须根据数据的重要性选择自己的参数。 (应选择PBKDF2_ROUNDS以适合您的主机系统。我已阅读somewhere哈希应该至少 241毫秒)

一些服务器端功能:

// 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');
}

现在我可以用这样一个唯一的密钥加密每个用户的密码:

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

解密也很简单:

let pass     = decryptUserPass(0, passEnc, salt);
// or
let pass2    = decryptUserPass(uid, passEnc);

说明

当然我知道,这在安全性方面仍然非常糟糕(在服务器上存储可以反转为用户密码的内容)。我认为这是可以的原因:

每个用户密码都加密如下:

AES(password, PBKDF2(global-encryption-key + salt))

这意味着:

  1. 每个用户的密码使用不同的密钥加密
  2. db
  3. 中没有保存加密密钥

    为什么我认为这是一个很好的解决方案:

    1. 如果数据库泄露,攻击者首先需要为一个特定用户正确猜测AES密钥,然后反转PBKDF2以找到global-encryption-key。或
    2. 猜猜global-encryption-key
    3. 因此,您应该选择一个相当大的全局加密密钥。

      关于盐的事实

      1. 永远不要使用盐两次
      2. 将盐与密码一起更改(不要重复使用)
      3. 盐应该很长:经验法则:使盐与哈希函数的输出一样长(sha256 = 32字节)
      4. more about salting things