在数据库中存储用户特定的 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))
这意味着:
- 每个用户的密码都使用不同的密钥加密
- 数据库中没有保存加密密钥
为什么我认为这是一个很好的解决方案:
- 万一数据库泄露,攻击者首先需要为特定用户正确猜测AES密钥,然后逆向PBKDF2找到全局加密密钥。或者
- 猜出全局加密密钥
因此您应该选择一个相当大的全局加密密钥。
关于盐的事实
- 切勿使用两次盐
- 更改盐和密码(不要重复使用)
- 盐应该很长:经验法则:使盐与散列函数的输出一样长(sha256 = 32 字节)
所以我正在使用 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))
这意味着:
- 每个用户的密码都使用不同的密钥加密
- 数据库中没有保存加密密钥
为什么我认为这是一个很好的解决方案:
- 万一数据库泄露,攻击者首先需要为特定用户正确猜测AES密钥,然后逆向PBKDF2找到全局加密密钥。或者
- 猜出全局加密密钥
因此您应该选择一个相当大的全局加密密钥。
关于盐的事实
- 切勿使用两次盐
- 更改盐和密码(不要重复使用)
- 盐应该很长:经验法则:使盐与散列函数的输出一样长(sha256 = 32 字节)