Last active
November 10, 2023 20:00
-
-
Save jordanbtucker/e9dde26b372048cf2cbe85a6aa9618de to your computer and use it in GitHub Desktop.
Database encryption with NeDB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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