在数据库中存储用户特定的 API 键

Storing user-specific API keys in DB

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

问题是,plain 需要密码。因此,密码必须在服务器上的某个时刻以明文形式访问。

我解决这个问题的方法:

我创建了一个 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 应用程序中使用这些设置,您必须像这样启动 meteor: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. 数据库中没有保存加密密钥

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

  1. 万一数据库泄露,攻击者首先需要为特定用户正确猜测AES密钥,然后逆向PBKDF2找到全局加密密钥。或者
  2. 猜出全局加密密钥

因此您应该选择一个相当大的全局加密密钥。

关于盐的事实

  1. 切勿使用两次盐
  2. 更改盐和密码(不要重复使用)
  3. 盐应该很长:经验法则:使盐与散列函数的输出一样长(sha256 = 32 字节)

more about salting things