Meteor mcrypt (atmospherejs.com)
Encrypt important data in your DB for later use. This uses the Node.js crypto library.
Each users data will be encrypted with a seperate key to make decryption harder in case of a data breach. The user-specific key (user-key) is derived from the application-key (app-key) plus a user-specific salt per PBKDF2.
IMPORTANT: This should NOT be used to encrypt passwords for your own login system. Often times, a simple hashing algorithm will serve you better. Especially in terms of security!
To install the package, run: meteor add antonly:mcrypt
.
Import it like this: import mcrypt from 'meteor/antonly:mcrypt'
.
Configure the package to exactly fit your needs
default settings:
mcrypt.configure({
getUserSalt(uid, context) {
// example method for getting the salt from the user db
const usr = Meteor.users.findOne(uid);
if (!usr || !usr.secret_key_storage || !usr.secret_key_storage.salt) {
throw new mcrypt.Error(
'no-salt-given',
`Salt for user with id '${userId}' was not found in the db.`,
{userId, context}
);
}
return usr.secret_key_storage.salt;
},
getAppKey(context) {
return Meteor.settings.MCRYPT_PASSW_ENCRYPTION_KEY;
},
getUserKeyLen(context) {
return Meteor.settings.MCRYPT_PASSW_USER_KEY_LENGTH;
},
getRounds(context) {
return Meteor.settings.MCRYPT_PBKDF2_ROUNDS;
},
getDigest(context) {
return Meteor.settings.MCRYPT_PBKDF2_DIGEST;
},
getAlgorithm(context) {
return Meteor.settings.MCRYPT_PASSW_ALGORITHM;
},
throwDecryptError(context) {
return Meteor.settings.MCRYPT_THROW_DECRYPT_ERR;
},
getSaltLen() {
return Meteor.settings.MCRYPT_PASSW_SALT_LENGTH;
}
})
getUserSalt(uid, context)
a function to get the users salt. (in this example it is taken from the Meteor.users db)getAppKey(context)
this function return the app-key. It is recommended that you don't change the following functions but rather add the following lines to your settings.json. All the following functions are just wrappers for theMeteor.settings
variables.
The context
wil be passed along from a decrypt
or encrypt
call to all settings methods (except getSaltLen
) and can help you customize your values to your specifc needs.
settings.json:
{
"MCRYPT_PASSW_ENCRYPTION_KEY": "*some long encryption key*",
"MCRYPT_PASSW_SALT_LENGTH": 32,
"MCRYPT_PASSW_USER_KEY_LENGTH": 32,
"MCRYPT_PBKDF2_ROUNDS": 100,
"MCRYPT_PBKDF2_DIGEST": "sha512",
"MCRYPT_PASSW_ALGORITHM": "aes-256-ctr",
"MCRYPT_THROW_DECRYPT_ERR": false
}
In order to be able to use these settings in your meteor app, you will have to add the settings parameter to your meteor
command: meteor --settings development.json
What each entry does:
MCRYPT_PASSW_ENCRYPTION_KEY
Your secret key (aka. app-key). This key should be impossible to guess, so chose a big one.MCRYPT_PASSW_SALT_LENGTH
The length (in bytes) of a standard 'salt' to derive the user-specific keyMCRYPT_PASSW_USER_KEY_LENGTH
The length (in bytes) of the key, derived from PBKDF2(app-key, salt) that is used to encrypt the dataMCRYPT_PBKDF2_ROUNDS
andMCRYPT_PBKDF2_DIGEST
correspond to their PBKDF2 parameters (more here)MCRYPT_PASSW_ALGORITHM
is the algorithm used to encrypt the data (the algorithm field of crypto.createCipher)MCRYPT_THROW_DECRYPT_ERR
Some algorithms will throw errors when trying to decipher with the wron key. If this is set to false, the error will be catched and an McryptError will be returned instead.
Sidenote: Of course you have to use 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)
If you want to set your own values, mcrypt.configure
expects an object with functions or values to replace the standard ones:
mcrypt.configure({
getUserSalt(uid, context) {
let db = AdminsDB;
if (context == 'clients') {
db = ClientsDB;
}
return db.find(uid).secret.salt;
},
getRounds: 10e9, // 10000000000
getSaltLen: 64
})
(notice how you can pass functions or values)
Encrypts the given cleartext.
cleartext
The utf-8 string to encryptuserId
the ID of the user who's salt will be usedsalt
can be passed instead ofuserId
if you already got it, or just generated it. If the salt is omitted or false, the result ofgetUserSalt(userId, context)
will be used as salt. The salt is used to derive the user-key from the app-keycontext
is passed along to thegetUserSalt(userId, context)
function in the case that no salt is provided directly
examples
import mcrypt from 'meteor/antonly:mcrypt';
const cleartext = 'Cleartext 1',
salt = 'Just some simple salt';
// encrypt with salt from DB
const ciphertext = mcrypt.encrypt(cleartext, 1);
// this will use the salt from user with the ID 1
// encrypt with known salt
const ciphertext2 = mcrypt.encrypt(cleartext, 0, salt);
// the userId can be set to any arbitrary value because the salt is provided
Decrypts given ciphertext.
ciphertext
the text to decryptuserId
same thing as before, either you specify a userId or a saltsalt
andcontext
see encrypt
This can return a bad-decrypt
error when MCRYPT_THROW_DECRYPT_ERR
is set to true. The error will look like this:
McryptError {
_isError: true,
code: 'bad-decrypt',
reason: 'The ciphertext couldn\'t be decrypted with the given key',
data: {
ciphertext: '2d27113bad83553254cd8a604282814e274ee4b08ccf57940e583f01a364b742',
salt: 'u/QOClVMrCOhESVa/pz0q6/plwCPVynoPMegKW3ArWw=',
userId: 'B',
context: { },
error: [Error: error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt]
}
}
data.error
contains the catched error.
Generates a random salt using crypto.randomBytes
. If you don't specify a length (in bytes) it will use the result of getSaltLen()
.
An error object. It has the following fields:
code
The error code (for example'no-salt-given'
)reason
a more in-depth explanation of what went wrongdata
(optional) Additional data for debugging or error handling / logging
example from the standard getUserSalt
method:
throw new mcrypt.Error(
'no-salt-given',
`Salt for user with id ${userId} was not found in the db.`,
{userId, context}
);
The error object will look something like this:
McryptError {
_isError: true,
code: 'no-salt-given',
reason: 'Salt for user with id 1 was not found in the db.',
data: { userId: 1, context: {} }
}
It also has a custom toString
method, wich will return [McryptError this.code]
. In this case it would be [McryptError no-salt-given]
.
This is how I (would) use this package. Your secret could be a user-specific API-key for example.
import mcrypt from 'meteor/antonly:mcrypt';
// point to the right field in the DB
mcrypt.configure({
getUserSalt(userId) {
const usr = Meteor.users.findOne(userId);
if (!usr || !usr.important || !usr.important.salt) {
throw new mcrypt.Error(
'no-salt-given',
`Salt for user with id '${userId}' was not found in the db.`,
{userId, context}
);
}
return usr.important.salt;
}
});
// make sure each user has a salt
Accounts.onCreateUser(function(options, user){
user.important = {
salt: mcrypt.generateSalt()
}
return user;
});
// implement methods to get and set secrets
Meteor.methods({
setSecret(secret) {
check(secret, String);
if (!this.userId) return {success:false, error: 'login-required'};
const cipher = mcrypt.encrypt(secret, this.userId);
Meteor.users.update(this.userId, {
$set: {
'important.secret': cipher
}
});
return cipher;
},
getSecret() {
if (!this.userId) return {success:false, error: 'login-required'};
const cipher = Meteor.users.findOne(this.userId).important.secret;
const clear = mcrypt.decrypt(cipher, this.userId);
return clear;
}
});
//// usage ////
// on the client
Meteor.call("setSecret", "secret", function (err, data) {
if (data && data.success !== false) alert(`Secret saved as "${data}"`);
})
// later on
Meteor.call("getSecret", function (err, data) {
if (!err) alert(`Secret retrieved as "${data}"`);
})
for more check mcrypt-test.js
.
There are a couple of TinyTest tests. To run them:
- Install TinyTest:
meteor add tinytest
- Run Meteor in test mode with your settings file:
meteor test-packages --settings settings.json
- Navigate to
http://localhost:3000
- Hopefully see all tests passing:
The code for this package is licensed under the MIT License.