Skip to content

Instantly share code, notes, and snippets.

@jordanbtucker
Last active November 10, 2023 20:00
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jordanbtucker/e9dde26b372048cf2cbe85a6aa9618de to your computer and use it in GitHub Desktop.
Save jordanbtucker/e9dde26b372048cf2cbe85a6aa9618de to your computer and use it in GitHub Desktop.
Database encryption with NeDB
/**
* This is an example of app that uses an ecrypted NeDB database. It prompts the
* user for a password, decrypts the database, displays any existing records,
* promtps the user for a new record to store, encrypts that record, then exits.
* The password must be given each time the app is started.
*/
const crypto = require('crypto')
const inquirer = require('inquirer')
const scrypt = require('scryptsy')
const nedb = require('nedb-promises')
// A salt is required for scrypt to derive an encryption key from a password. A
// salt is a random value used to mitigate rainbow tables. It does not need to
// be kept secret, but it needs to be consitent as only the same password and
// salt combination will result in the same key. In this example, the same salt
// is used for all instances of this app, which means that the encrypted
// database is portable. But this also makes this app more susceptible to
// rainbow tables.
const dbSalt = Buffer.from('GagZgR/G2isc0IbKKYyFLg==')
// Alternatively, you can generate a salt on the first run of the app and store
// it somewhere. This will make the app more resilient to rainbow tables, but
// the encrypted database will no longer be portable.
// This is the main function. It will be called at the end of the file.
async function run() {
// Propmpt the user for the database encryption password. This, along with the
// salt, will be used by scrypt to generate an encryption key for the database.
// The same password must be used each time the app runs. If the password is
// lost or forgotten, then the database cannot be decrypted or recovered.
const {dbPass} = await inquirer.prompt([
{
name: 'dbPass',
type: 'password',
message: 'Database password:',
},
])
// scrypt uses the password, salt, and other parameters to derive the
// encryption key. The other parameters determine how much time it takes the
// CPU to derive the key, which mitigates brute force attacks, except for the
// last parameter which specifies the length of the key in bytes. A change to
// any of these parameters will result in a different key.
const key = scrypt(dbPass, dbSalt, 32768, 8, 1, 32)
// We're using nedb-promises so that we can await the database operations, but
// regular nedb works too.
const db = nedb.create({
filename: 'example.db',
// This is the encryption function. It takes plaintext, which is JSON,
// encrypts it with the derived key, and returns the encrypted ciphertext as
// a Base 64 string. A random IV is generated and stored for each record to
// mitigate padding attacks. Note that you don't need to return JSON; any
// string will do.
afterSerialization(plaintext) {
const iv = crypto.randomBytes(16)
const aes = crypto.createCipheriv('aes-256-cbc', key, iv)
let ciphertext = aes.update(plaintext)
ciphertext = Buffer.concat([iv, ciphertext, aes.final()])
return ciphertext.toString('base64')
},
// This is the decryption function. It takes the encrypted ciphertext,
// decrypts it with the stored IV and derived key, and returns the decrypted
// plaintext, which is JSON. Note that this function must return JSON, since
// that is what NeDB expects.
beforeDeserialization(ciphertext) {
const ciphertextBytes = Buffer.from(ciphertext, 'base64')
const iv = ciphertextBytes.slice(0, 16)
const data = ciphertextBytes.slice(16)
const aes = crypto.createDecipheriv('aes-256-cbc', key, iv)
let plaintextBytes = Buffer.from(aes.update(data))
plaintextBytes = Buffer.concat([plaintextBytes, aes.final()])
return plaintextBytes.toString()
},
})
// In the event that the wrong password is entered, when NeDB tries to decrypt
// the records, it will be given garbage (i.e. not JSON) so it will return an
// error indicating the the database is corrupt.
// Display the currently stored values, if any, which were decrypted from the
// database file.
console.log('Current values stored in database: ')
const rows = await db.find()
for (const row of rows) {
console.log(`${row.date}: ${row.value}`)
}
// Prompt for a new value to store, which is stored along with a timestamp.
console.log('Store a new value in the database.')
const {value} = await inquirer.prompt([
{
name: 'value',
type: 'input',
message: 'Value:',
},
])
await db.insert({value, date: new Date()})
console.log('Value stored. Restart the app to see the values.')
}
// Run the async main function. If there are any errors, report them and close
// the process with an exit code.
run().catch(err => {
console.error(err)
process.exitCode = err.code || 1
})
/**
* This is an example of app that uses an ecrypted NeDB database. It prompts the
* user for a password, decrypts the database, displays any existing records,
* promtps the user for a new record to store, encrypts that record, then exits.
* The password must be given each time the app is started.
*/
const crypto = require('crypto')
const keytar = require('keytar')
const inquirer = require('inquirer')
const scrypt = require('scryptsy')
const nedb = require('nedb-promises')
// A salt is required for scrypt to derive an encryption key from a password. A
// salt is a random value used to mitigate rainbow tables. It does not need to
// be kept secret, but it needs to be consitent as only the same password and
// salt combination will result in the same key. In this example, the same salt
// is used for all instances of this app, which means that the encrypted
// database is portable. But this also makes this app more susceptible to
// rainbow tables.
const dbSalt = Buffer.from('GagZgR/G2isc0IbKKYyFLg==')
// Alternatively, you can generate a salt on the first run of the app and store
// it somewhere. This will make the app more resilient to rainbow tables, but
// the encrypted database will no longer be portable.
// This is the main function. It will be called at the end of the file.
async function run() {
// Retrieve the database encryption password from the system keychain. If no
// password exists in the keychain, prompt the user for one, then store it in
// the keychain. This, along with the salt, will be used by scrypt to generate
// an encryption key for the database. The same password must be used each
// time the app runs. If the password is lost or forgotten, then the database
// cannot be decrypted or recovered.
let dbPass = await keytar.getPassword('nedb-example', 'dbPass')
if (dbPass == null) {
const {userDBPass} = await inquirer.prompt([
{
name: 'userDBPass',
type: 'password',
message: 'Database password:',
},
])
dbPass = userDBPass
await keytar.setPassword('nedb-example', 'dbPass', dbPass)
}
// scrypt uses the password, salt, and other parameters to derive the
// encryption key. The other parameters determine how much time it takes the
// CPU to derive the key, which mitigates brute force attacks, except for the
// last parameter which specifies the length of the key in bytes. A change to
// any of these parameters will result in a different key.
const key = scrypt(dbPass, dbSalt, 32768, 8, 1, 32)
// We're using nedb-promises so that we can await the database operations, but
// regular nedb works too.
const db = nedb.create({
filename: 'example.db',
// This is the encryption function. It takes plaintext, which is JSON,
// encrypts it with the derived key, and returns the encrypted ciphertext as
// a Base 64 string. A random IV is generated and stored for each record to
// mitigate padding attacks. Note that you don't need to return JSON; any
// string will do.
afterSerialization(plaintext) {
const iv = crypto.randomBytes(16)
const aes = crypto.createCipheriv('aes-256-cbc', key, iv)
let ciphertext = aes.update(plaintext)
ciphertext = Buffer.concat([iv, ciphertext, aes.final()])
return ciphertext.toString('base64')
},
// This is the decryption function. It takes the encrypted ciphertext,
// decrypts it with the stored IV and derived key, and returns the decrypted
// plaintext, which is JSON. Note that this function must return JSON, since
// that is what NeDB expects.
beforeDeserialization(ciphertext) {
const ciphertextBytes = Buffer.from(ciphertext, 'base64')
const iv = ciphertextBytes.slice(0, 16)
const data = ciphertextBytes.slice(16)
const aes = crypto.createDecipheriv('aes-256-cbc', key, iv)
let plaintextBytes = Buffer.from(aes.update(data))
plaintextBytes = Buffer.concat([plaintextBytes, aes.final()])
return plaintextBytes.toString()
},
})
// In the event that the wrong password is entered, when NeDB tries to decrypt
// the records, it will be given garbage (i.e. not JSON) so it will return an
// error indicating the the database is corrupt.
// Display the currently stored values, if any, which were decrypted from the
// database file.
console.log('Current values stored in database: ')
const rows = await db.find()
for (const row of rows) {
console.log(`${row.date}: ${row.value}`)
}
// Prompt for a new value to store, which is stored along with a timestamp.
console.log('Store a new value in the database.')
const {value} = await inquirer.prompt([
{
name: 'value',
type: 'input',
message: 'Value:',
},
])
await db.insert({value, date: new Date()})
console.log('Value stored. Restart the app to see the values.')
}
// Run the async main function. If there are any errors, report them and close
// the process with an exit code.
run().catch(err => {
console.error(err)
process.exitCode = err.code || 1
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment